blob: 61c92018a4062bda28f2dbfd7e0667ae08a3f940 [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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.graphics.Rect;
George Mount140ea622015-10-27 08:46:44 -070022import android.util.ArrayMap;
23import android.util.SparseArray;
24import android.util.SparseBooleanArray;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025
26import java.util.ArrayList;
Jeff Brown4e6319b2010-12-13 10:36:51 -080027import java.util.Collections;
28import java.util.Comparator;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -080029import java.util.List;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030
31/**
32 * The algorithm used for finding the next focusable view in a given direction
33 * from a view that currently has focus.
34 */
35public class FocusFinder {
36
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070037 private static final ThreadLocal<FocusFinder> tlFocusFinder =
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038 new ThreadLocal<FocusFinder>() {
Svetoslav Ganov42138042012-03-20 11:51:39 -070039 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040 protected FocusFinder initialValue() {
41 return new FocusFinder();
42 }
43 };
44
45 /**
46 * Get the focus finder for this thread.
47 */
48 public static FocusFinder getInstance() {
49 return tlFocusFinder.get();
50 }
51
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070052 final Rect mFocusedRect = new Rect();
53 final Rect mOtherRect = new Rect();
54 final Rect mBestCandidateRect = new Rect();
Evan Rosky3b94bf52017-01-10 17:05:28 -080055 private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator =
56 new UserSpecifiedFocusComparator();
57 private final FocusComparator mFocusComparator = new FocusComparator();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080058
Svetoslav Ganov42138042012-03-20 11:51:39 -070059 private final ArrayList<View> mTempList = new ArrayList<View>();
60
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061 // enforce thread local access
62 private FocusFinder() {}
63
64 /**
65 * Find the next view to take focus in root's descendants, starting from the view
66 * that currently is focused.
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -070067 * @param root Contains focused. Cannot be null.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068 * @param focused Has focus now.
69 * @param direction Direction to look.
70 * @return The next focusable view, or null if none exists.
71 */
72 public final View findNextFocus(ViewGroup root, View focused, int direction) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -070073 return findNextFocus(root, focused, null, direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -070074 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080075
Svetoslav Ganov42138042012-03-20 11:51:39 -070076 /**
77 * Find the next view to take focus in root's descendants, searching from
78 * a particular rectangle in root's coordinates.
79 * @param root Contains focusedRect. Cannot be null.
80 * @param focusedRect The starting point of the search.
81 * @param direction Direction to look.
82 * @return The next focusable view, or null if none exists.
83 */
84 public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070085 mFocusedRect.set(focusedRect);
86 return findNextFocus(root, null, mFocusedRect, direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -070087 }
88
89 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070090 View next = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080091 if (focused != null) {
Svetoslav Ganov27e2da72012-07-02 18:12:00 -070092 next = findNextUserSpecifiedFocus(root, focused, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070093 }
94 if (next != null) {
95 return next;
96 }
97 ArrayList<View> focusables = mTempList;
98 try {
99 focusables.clear();
100 root.addFocusables(focusables, direction);
101 if (!focusables.isEmpty()) {
102 next = findNextFocus(root, focused, focusedRect, direction, focusables);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800103 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700104 } finally {
105 focusables.clear();
106 }
107 return next;
108 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800109
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800110 /**
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800111 * Find the root of the next keyboard navigation cluster after the current one.
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800112 * @param root The view tree to look inside. Cannot be null
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800113 * @param currentCluster The starting point of the search. Null means the default cluster
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800114 * @param direction Direction to look
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800115 * @return The next cluster, or null if none exists
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800116 */
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800117 public View findNextKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800118 @NonNull View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800119 @Nullable View currentCluster,
Evan Rosky57223312017-02-08 14:42:45 -0800120 @View.FocusDirection int direction) {
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800121 View next = null;
122
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800123 final ArrayList<View> clusters = mTempList;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800124 try {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800125 clusters.clear();
126 root.addKeyboardNavigationClusters(clusters, direction);
127 if (!clusters.isEmpty()) {
128 next = findNextKeyboardNavigationCluster(
129 root, currentCluster, clusters, direction);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800130 }
131 } finally {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800132 clusters.clear();
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800133 }
134 return next;
135 }
136
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700137 private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700138 // check for user specified next focus
139 View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
140 if (userSetNextFocus != null && userSetNextFocus.isFocusable()
141 && (!userSetNextFocus.isInTouchMode()
142 || userSetNextFocus.isFocusableInTouchMode())) {
143 return userSetNextFocus;
144 }
145 return null;
146 }
147
148 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
149 int direction, ArrayList<View> focusables) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700150 if (focused != null) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700151 if (focusedRect == null) {
152 focusedRect = mFocusedRect;
153 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800154 // fill in interesting rect from focused
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700155 focused.getFocusedRect(focusedRect);
156 root.offsetDescendantRectToMyCoords(focused, focusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800157 } else {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700158 if (focusedRect == null) {
159 focusedRect = mFocusedRect;
160 // make up a rect at top left or bottom right of root
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700161 switch (direction) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700162 case View.FOCUS_RIGHT:
163 case View.FOCUS_DOWN:
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700164 setFocusTopLeft(root, focusedRect);
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700165 break;
166 case View.FOCUS_FORWARD:
167 if (root.isLayoutRtl()) {
168 setFocusBottomRight(root, focusedRect);
169 } else {
170 setFocusTopLeft(root, focusedRect);
171 }
172 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700174 case View.FOCUS_LEFT:
175 case View.FOCUS_UP:
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700176 setFocusBottomRight(root, focusedRect);
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700177 break;
178 case View.FOCUS_BACKWARD:
179 if (root.isLayoutRtl()) {
180 setFocusTopLeft(root, focusedRect);
181 } else {
182 setFocusBottomRight(root, focusedRect);
183 break;
184 }
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700185 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800186 }
187 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700188
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700189 switch (direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700190 case View.FOCUS_FORWARD:
191 case View.FOCUS_BACKWARD:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700192 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
193 direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700194 case View.FOCUS_UP:
195 case View.FOCUS_DOWN:
196 case View.FOCUS_LEFT:
197 case View.FOCUS_RIGHT:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700198 return findNextFocusInAbsoluteDirection(focusables, root, focused,
199 focusedRect, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700200 default:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700201 throw new IllegalArgumentException("Unknown direction: " + direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700202 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700203 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700204
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800205 private View findNextKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800206 View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800207 View currentCluster,
208 List<View> clusters,
Evan Rosky57223312017-02-08 14:42:45 -0800209 @View.FocusDirection int direction) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800210 final int count = clusters.size();
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800211
212 switch (direction) {
213 case View.FOCUS_FORWARD:
214 case View.FOCUS_DOWN:
215 case View.FOCUS_RIGHT:
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800216 return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800217 case View.FOCUS_BACKWARD:
218 case View.FOCUS_UP:
219 case View.FOCUS_LEFT:
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800220 return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800221 default:
222 throw new IllegalArgumentException("Unknown direction: " + direction);
223 }
224 }
225
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700226 private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700227 View focused, Rect focusedRect, int direction) {
228 try {
229 // Note: This sort is stable.
Evan Rosky3b94bf52017-01-10 17:05:28 -0800230 mUserSpecifiedFocusComparator.setFocusables(focusables);
231 Collections.sort(focusables, mUserSpecifiedFocusComparator);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700232 } finally {
Evan Rosky3b94bf52017-01-10 17:05:28 -0800233 mUserSpecifiedFocusComparator.recycle();
Svetoslav Ganov42138042012-03-20 11:51:39 -0700234 }
235
236 final int count = focusables.size();
237 switch (direction) {
238 case View.FOCUS_FORWARD:
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700239 return getNextFocusable(focused, focusables, count);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700240 case View.FOCUS_BACKWARD:
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700241 return getPreviousFocusable(focused, focusables, count);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700242 }
243 return focusables.get(count - 1);
244 }
245
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700246 private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700247 final int rootBottom = root.getScrollY() + root.getHeight();
248 final int rootRight = root.getScrollX() + root.getWidth();
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700249 focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700250 }
251
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700252 private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700253 final int rootTop = root.getScrollY();
254 final int rootLeft = root.getScrollX();
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700255 focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700256 }
257
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700258 View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700259 Rect focusedRect, int direction) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800260 // initialize the best candidate to something impossible
261 // (so the first plausible view will become the best choice)
262 mBestCandidateRect.set(focusedRect);
263 switch(direction) {
264 case View.FOCUS_LEFT:
265 mBestCandidateRect.offset(focusedRect.width() + 1, 0);
266 break;
267 case View.FOCUS_RIGHT:
268 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
269 break;
270 case View.FOCUS_UP:
271 mBestCandidateRect.offset(0, focusedRect.height() + 1);
272 break;
273 case View.FOCUS_DOWN:
274 mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
275 }
276
277 View closest = null;
278
279 int numFocusables = focusables.size();
280 for (int i = 0; i < numFocusables; i++) {
281 View focusable = focusables.get(i);
282
283 // only interested in other non-root views
284 if (focusable == focused || focusable == root) continue;
285
Tobias Duboisdefdb1e2010-12-15 11:35:30 +0100286 // get focus bounds of other view in same coordinate system
Fabrice Di Meglioc11f77f2012-09-18 15:33:07 -0700287 focusable.getFocusedRect(mOtherRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800288 root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
289
290 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
291 mBestCandidateRect.set(mOtherRect);
292 closest = focusable;
293 }
294 }
295 return closest;
296 }
297
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700298 private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700299 if (focused != null) {
300 int position = focusables.lastIndexOf(focused);
301 if (position >= 0 && position + 1 < count) {
302 return focusables.get(position + 1);
303 }
304 }
Svetoslav Ganove5dfa47d2012-05-08 15:58:32 -0700305 if (!focusables.isEmpty()) {
306 return focusables.get(0);
307 }
308 return null;
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700309 }
310
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700311 private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700312 if (focused != null) {
313 int position = focusables.indexOf(focused);
314 if (position > 0) {
315 return focusables.get(position - 1);
316 }
317 }
Svetoslav Ganove5dfa47d2012-05-08 15:58:32 -0700318 if (!focusables.isEmpty()) {
319 return focusables.get(count - 1);
320 }
321 return null;
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700322 }
323
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800324 private static View getNextKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800325 View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800326 View currentCluster,
327 List<View> clusters,
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800328 int count) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800329 if (currentCluster == null) {
330 // The current cluster is the default one.
331 // The next cluster after the default one is the first one.
332 // Note that the caller guarantees that 'clusters' is not empty.
333 return clusters.get(0);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800334 }
335
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800336 final int position = clusters.lastIndexOf(currentCluster);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800337 if (position >= 0 && position + 1 < count) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800338 // Return the next non-default cluster if we can find it.
339 return clusters.get(position + 1);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800340 }
341
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800342 // The current cluster is the last one. The next one is the default one, i.e. the
343 // root.
344 return root;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800345 }
346
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800347 private static View getPreviousKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800348 View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800349 View currentCluster,
350 List<View> clusters,
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800351 int count) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800352 if (currentCluster == null) {
353 // The current cluster is the default one.
354 // The previous cluster before the default one is the last one.
355 // Note that the caller guarantees that 'clusters' is not empty.
356 return clusters.get(count - 1);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800357 }
358
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800359 final int position = clusters.indexOf(currentCluster);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800360 if (position > 0) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800361 // Return the previous non-default cluster if we can find it.
362 return clusters.get(position - 1);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800363 }
364
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800365 // The current cluster is the first one. The previous one is the default one, i.e.
366 // the root.
367 return root;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800368 }
369
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800370 /**
371 * Is rect1 a better candidate than rect2 for a focus search in a particular
372 * direction from a source rect? This is the core routine that determines
373 * the order of focus searching.
374 * @param direction the direction (up, down, left, right)
375 * @param source The source we are searching from
376 * @param rect1 The candidate rectangle
377 * @param rect2 The current best candidate.
378 * @return Whether the candidate is the new best.
379 */
380 boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
381
382 // to be a better candidate, need to at least be a candidate in the first
383 // place :)
384 if (!isCandidate(source, rect1, direction)) {
385 return false;
386 }
387
388 // we know that rect1 is a candidate.. if rect2 is not a candidate,
389 // rect1 is better
390 if (!isCandidate(source, rect2, direction)) {
391 return true;
392 }
393
394 // if rect1 is better by beam, it wins
395 if (beamBeats(direction, source, rect1, rect2)) {
396 return true;
397 }
398
399 // if rect2 is better, then rect1 cant' be :)
400 if (beamBeats(direction, source, rect2, rect1)) {
401 return false;
402 }
403
404 // otherwise, do fudge-tastic comparison of the major and minor axis
405 return (getWeightedDistanceFor(
406 majorAxisDistance(direction, source, rect1),
407 minorAxisDistance(direction, source, rect1))
408 < getWeightedDistanceFor(
409 majorAxisDistance(direction, source, rect2),
410 minorAxisDistance(direction, source, rect2)));
411 }
412
413 /**
414 * One rectangle may be another candidate than another by virtue of being
415 * exclusively in the beam of the source rect.
416 * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's
417 * beam
418 */
419 boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
420 final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
421 final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
422
423 // if rect1 isn't exclusively in the src beam, it doesn't win
424 if (rect2InSrcBeam || !rect1InSrcBeam) {
425 return false;
426 }
427
428 // we know rect1 is in the beam, and rect2 is not
429
430 // if rect1 is to the direction of, and rect2 is not, rect1 wins.
431 // for example, for direction left, if rect1 is to the left of the source
432 // and rect2 is below, then we always prefer the in beam rect1, since rect2
433 // could be reached by going down.
434 if (!isToDirectionOf(direction, source, rect2)) {
435 return true;
436 }
437
438 // for horizontal directions, being exclusively in beam always wins
439 if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
440 return true;
441 }
442
443 // for vertical directions, beams only beat up to a point:
444 // now, as long as rect2 isn't completely closer, rect1 wins
445 // e.g for direction down, completely closer means for rect2's top
446 // edge to be closer to the source's top edge than rect1's bottom edge.
447 return (majorAxisDistance(direction, source, rect1)
448 < majorAxisDistanceToFarEdge(direction, source, rect2));
449 }
450
451 /**
452 * Fudge-factor opportunity: how to calculate distance given major and minor
453 * axis distances. Warning: this fudge factor is finely tuned, be sure to
454 * run all focus tests if you dare tweak it.
455 */
456 int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
457 return 13 * majorAxisDistance * majorAxisDistance
458 + minorAxisDistance * minorAxisDistance;
459 }
460
461 /**
462 * Is destRect a candidate for the next focus given the direction? This
463 * checks whether the dest is at least partially to the direction of (e.g left of)
464 * from source.
465 *
466 * Includes an edge case for an empty rect (which is used in some cases when
467 * searching from a point on the screen).
468 */
469 boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
470 switch (direction) {
471 case View.FOCUS_LEFT:
472 return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
473 && srcRect.left > destRect.left;
474 case View.FOCUS_RIGHT:
475 return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
476 && srcRect.right < destRect.right;
477 case View.FOCUS_UP:
478 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
479 && srcRect.top > destRect.top;
480 case View.FOCUS_DOWN:
481 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
482 && srcRect.bottom < destRect.bottom;
483 }
484 throw new IllegalArgumentException("direction must be one of "
485 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
486 }
487
488
489 /**
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700490 * 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 -0800491 * @param direction the direction (up, down, left, right)
492 * @param rect1 The first rectangle
493 * @param rect2 The second rectangle
494 * @return whether the beams overlap
495 */
496 boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
497 switch (direction) {
498 case View.FOCUS_LEFT:
499 case View.FOCUS_RIGHT:
500 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
501 case View.FOCUS_UP:
502 case View.FOCUS_DOWN:
503 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
504 }
505 throw new IllegalArgumentException("direction must be one of "
506 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
507 }
508
509 /**
510 * e.g for left, is 'to left of'
511 */
512 boolean isToDirectionOf(int direction, Rect src, Rect dest) {
513 switch (direction) {
514 case View.FOCUS_LEFT:
515 return src.left >= dest.right;
516 case View.FOCUS_RIGHT:
517 return src.right <= dest.left;
518 case View.FOCUS_UP:
519 return src.top >= dest.bottom;
520 case View.FOCUS_DOWN:
521 return src.bottom <= dest.top;
522 }
523 throw new IllegalArgumentException("direction must be one of "
524 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
525 }
526
527 /**
528 * @return The distance from the edge furthest in the given direction
529 * of source to the edge nearest in the given direction of dest. If the
530 * dest is not in the direction from source, return 0.
531 */
532 static int majorAxisDistance(int direction, Rect source, Rect dest) {
533 return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
534 }
535
536 static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
537 switch (direction) {
538 case View.FOCUS_LEFT:
539 return source.left - dest.right;
540 case View.FOCUS_RIGHT:
541 return dest.left - source.right;
542 case View.FOCUS_UP:
543 return source.top - dest.bottom;
544 case View.FOCUS_DOWN:
545 return dest.top - source.bottom;
546 }
547 throw new IllegalArgumentException("direction must be one of "
548 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
549 }
550
551 /**
552 * @return The distance along the major axis w.r.t the direction from the
553 * edge of source to the far edge of dest. If the
554 * dest is not in the direction from source, return 1 (to break ties with
555 * {@link #majorAxisDistance}).
556 */
557 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
558 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
559 }
560
561 static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
562 switch (direction) {
563 case View.FOCUS_LEFT:
564 return source.left - dest.left;
565 case View.FOCUS_RIGHT:
566 return dest.right - source.right;
567 case View.FOCUS_UP:
568 return source.top - dest.top;
569 case View.FOCUS_DOWN:
570 return dest.bottom - source.bottom;
571 }
572 throw new IllegalArgumentException("direction must be one of "
573 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
574 }
575
576 /**
577 * Find the distance on the minor axis w.r.t the direction to the nearest
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700578 * edge of the destination rectangle.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800579 * @param direction the direction (up, down, left, right)
580 * @param source The source rect.
581 * @param dest The destination rect.
582 * @return The distance.
583 */
584 static int minorAxisDistance(int direction, Rect source, Rect dest) {
585 switch (direction) {
586 case View.FOCUS_LEFT:
587 case View.FOCUS_RIGHT:
588 // the distance between the center verticals
589 return Math.abs(
590 ((source.top + source.height() / 2) -
591 ((dest.top + dest.height() / 2))));
592 case View.FOCUS_UP:
593 case View.FOCUS_DOWN:
594 // the distance between the center horizontals
595 return Math.abs(
596 ((source.left + source.width() / 2) -
597 ((dest.left + dest.width() / 2))));
598 }
599 throw new IllegalArgumentException("direction must be one of "
600 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
601 }
602
603 /**
604 * Find the nearest touchable view to the specified view.
605 *
606 * @param root The root of the tree in which to search
607 * @param x X coordinate from which to start the search
608 * @param y Y coordinate from which to start the search
609 * @param direction Direction to look
610 * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array
611 * may already be populated with values.
612 * @return The nearest touchable view, or null if none exists.
613 */
614 public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) {
615 ArrayList<View> touchables = root.getTouchables();
616 int minDistance = Integer.MAX_VALUE;
617 View closest = null;
618
619 int numTouchables = touchables.size();
620
621 int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop();
622
623 Rect closestBounds = new Rect();
624 Rect touchableBounds = mOtherRect;
625
626 for (int i = 0; i < numTouchables; i++) {
627 View touchable = touchables.get(i);
628
629 // get visible bounds of other view in same coordinate system
630 touchable.getDrawingRect(touchableBounds);
631
632 root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true);
633
634 if (!isTouchCandidate(x, y, touchableBounds, direction)) {
635 continue;
636 }
637
638 int distance = Integer.MAX_VALUE;
639
640 switch (direction) {
641 case View.FOCUS_LEFT:
642 distance = x - touchableBounds.right + 1;
643 break;
644 case View.FOCUS_RIGHT:
645 distance = touchableBounds.left;
646 break;
647 case View.FOCUS_UP:
648 distance = y - touchableBounds.bottom + 1;
649 break;
650 case View.FOCUS_DOWN:
651 distance = touchableBounds.top;
652 break;
653 }
654
655 if (distance < edgeSlop) {
656 // Give preference to innermost views
657 if (closest == null ||
658 closestBounds.contains(touchableBounds) ||
659 (!touchableBounds.contains(closestBounds) && distance < minDistance)) {
660 minDistance = distance;
661 closest = touchable;
662 closestBounds.set(touchableBounds);
663 switch (direction) {
664 case View.FOCUS_LEFT:
665 deltas[0] = -distance;
666 break;
667 case View.FOCUS_RIGHT:
668 deltas[0] = distance;
669 break;
670 case View.FOCUS_UP:
671 deltas[1] = -distance;
672 break;
673 case View.FOCUS_DOWN:
674 deltas[1] = distance;
675 break;
676 }
677 }
678 }
679 }
680 return closest;
681 }
682
683
684 /**
685 * Is destRect a candidate for the next touch given the direction?
686 */
687 private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) {
688 switch (direction) {
689 case View.FOCUS_LEFT:
690 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom;
691 case View.FOCUS_RIGHT:
692 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom;
693 case View.FOCUS_UP:
694 return destRect.top <= y && destRect.left <= x && x <= destRect.right;
695 case View.FOCUS_DOWN:
696 return destRect.top >= y && destRect.left <= x && x <= destRect.right;
697 }
698 throw new IllegalArgumentException("direction must be one of "
699 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
700 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800701
George Mount140ea622015-10-27 08:46:44 -0700702 private static final boolean isValidId(final int id) {
703 return id != 0 && id != View.NO_ID;
704 }
705
Evan Rosky3b94bf52017-01-10 17:05:28 -0800706 static FocusComparator getFocusComparator(ViewGroup root, boolean isRtl) {
707 FocusComparator comparator = getInstance().mFocusComparator;
708 comparator.setRoot(root);
709 comparator.setIsLayoutRtl(isRtl);
710 return comparator;
711 }
712
713 static final class FocusComparator implements Comparator<View> {
Jeff Brown4e6319b2010-12-13 10:36:51 -0800714 private final Rect mFirstRect = new Rect();
715 private final Rect mSecondRect = new Rect();
Evan Rosky3b94bf52017-01-10 17:05:28 -0800716 private ViewGroup mRoot = null;
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700717 private boolean mIsLayoutRtl;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800718
Evan Rosky3b94bf52017-01-10 17:05:28 -0800719 public void setIsLayoutRtl(boolean b) {
720 mIsLayoutRtl = b;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800721 }
722
723 public void setRoot(ViewGroup root) {
724 mRoot = root;
725 }
726
Evan Rosky3b94bf52017-01-10 17:05:28 -0800727 public int compare(View first, View second) {
728 if (first == second) {
729 return 0;
730 }
731
732 getRect(first, mFirstRect);
733 getRect(second, mSecondRect);
734
Evan Roskye4667522017-03-09 20:32:49 +0000735 if (mFirstRect.top < mSecondRect.top) {
736 return -1;
737 } else if (mFirstRect.top > mSecondRect.top) {
738 return 1;
739 } else if (mFirstRect.left < mSecondRect.left) {
740 return mIsLayoutRtl ? 1 : -1;
741 } else if (mFirstRect.left > mSecondRect.left) {
742 return mIsLayoutRtl ? -1 : 1;
743 } else if (mFirstRect.bottom < mSecondRect.bottom) {
744 return -1;
745 } else if (mFirstRect.bottom > mSecondRect.bottom) {
746 return 1;
747 } else if (mFirstRect.right < mSecondRect.right) {
748 return mIsLayoutRtl ? 1 : -1;
749 } else if (mFirstRect.right > mSecondRect.right) {
750 return mIsLayoutRtl ? -1 : 1;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800751 } else {
Evan Roskye4667522017-03-09 20:32:49 +0000752 // The view are distinct but completely coincident so we consider
753 // them equal for our purposes. Since the sort is stable, this
754 // means that the views will retain their layout order relative to one another.
755 return 0;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800756 }
757 }
758
759 private void getRect(View view, Rect rect) {
760 view.getDrawingRect(rect);
761 mRoot.offsetDescendantRectToMyCoords(view, rect);
762 }
763 }
764
765 /**
766 * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
767 * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
768 */
769 private static final class UserSpecifiedFocusComparator implements Comparator<View> {
770 private final SparseArray<View> mFocusables = new SparseArray<View>();
771 private final SparseBooleanArray mIsConnectedTo = new SparseBooleanArray();
772 private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>();
773 private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>();
774
775 public void recycle() {
776 mFocusables.clear();
777 mHeadsOfChains.clear();
778 mIsConnectedTo.clear();
779 mOriginalOrdinal.clear();
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700780 }
781
George Mount140ea622015-10-27 08:46:44 -0700782 public void setFocusables(ArrayList<View> focusables) {
783 for (int i = focusables.size() - 1; i >= 0; i--) {
784 final View view = focusables.get(i);
785 final int id = view.getId();
786 if (isValidId(id)) {
787 mFocusables.put(id, view);
788 }
789 final int nextId = view.getNextFocusForwardId();
790 if (isValidId(nextId)) {
791 mIsConnectedTo.put(nextId, true);
792 }
793 }
794
795 for (int i = focusables.size() - 1; i >= 0; i--) {
796 final View view = focusables.get(i);
797 final int nextId = view.getNextFocusForwardId();
798 if (isValidId(nextId) && !mIsConnectedTo.get(view.getId())) {
799 setHeadOfChain(view);
800 }
801 }
Evan Rosky3b94bf52017-01-10 17:05:28 -0800802
803 for (int i = 0; i < focusables.size(); ++i) {
804 mOriginalOrdinal.put(focusables.get(i), i);
805 }
George Mount140ea622015-10-27 08:46:44 -0700806 }
807
808 private void setHeadOfChain(View head) {
809 for (View view = head; view != null;
810 view = mFocusables.get(view.getNextFocusForwardId())) {
811 final View otherHead = mHeadsOfChains.get(view);
812 if (otherHead != null) {
813 if (otherHead == head) {
814 return; // This view has already had its head set properly
815 }
816 // A hydra -- multi-headed focus chain (e.g. A->C and B->C)
817 // Use the one we've already chosen instead and reset this chain.
818 view = head;
819 head = otherHead;
820 }
821 mHeadsOfChains.put(view, head);
822 }
823 }
824
Jeff Brown4e6319b2010-12-13 10:36:51 -0800825 public int compare(View first, View second) {
826 if (first == second) {
827 return 0;
828 }
George Mount140ea622015-10-27 08:46:44 -0700829 // Order between views within a chain is immaterial -- next/previous is
830 // within a chain is handled elsewhere.
831 View firstHead = mHeadsOfChains.get(first);
832 View secondHead = mHeadsOfChains.get(second);
833 if (firstHead == secondHead && firstHead != null) {
834 if (first == firstHead) {
835 return -1; // first is the head, it should be first
836 } else if (second == firstHead) {
837 return 1; // second is the head, it should be first
838 } else if (isValidId(first.getNextFocusForwardId())) {
839 return -1; // first is not the end of the chain
840 } else {
841 return 1; // first is end of chain
842 }
843 }
Evan Rosky3b94bf52017-01-10 17:05:28 -0800844 boolean involvesChain = false;
George Mount140ea622015-10-27 08:46:44 -0700845 if (firstHead != null) {
846 first = firstHead;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800847 involvesChain = true;
George Mount140ea622015-10-27 08:46:44 -0700848 }
849 if (secondHead != null) {
850 second = secondHead;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800851 involvesChain = true;
George Mount140ea622015-10-27 08:46:44 -0700852 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800853
Evan Rosky3b94bf52017-01-10 17:05:28 -0800854 if (involvesChain) {
855 // keep original order between chains
856 return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800857 } else {
Jeff Brown4e6319b2010-12-13 10:36:51 -0800858 return 0;
859 }
860 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800861 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800862}