The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package android.view; |
| 18 | |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 19 | import android.annotation.NonNull; |
| 20 | import android.annotation.Nullable; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 21 | import android.graphics.Rect; |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 22 | import android.util.ArrayMap; |
| 23 | import android.util.SparseArray; |
| 24 | import android.util.SparseBooleanArray; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 25 | |
| 26 | import java.util.ArrayList; |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 27 | import java.util.Collections; |
| 28 | import java.util.Comparator; |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 29 | import java.util.List; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 30 | |
| 31 | /** |
| 32 | * The algorithm used for finding the next focusable view in a given direction |
| 33 | * from a view that currently has focus. |
| 34 | */ |
| 35 | public class FocusFinder { |
| 36 | |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 37 | private static final ThreadLocal<FocusFinder> tlFocusFinder = |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 38 | new ThreadLocal<FocusFinder>() { |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 39 | @Override |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 40 | 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 Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 52 | final Rect mFocusedRect = new Rect(); |
| 53 | final Rect mOtherRect = new Rect(); |
| 54 | final Rect mBestCandidateRect = new Rect(); |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 55 | private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator = |
| 56 | new UserSpecifiedFocusComparator(); |
| 57 | private final FocusComparator mFocusComparator = new FocusComparator(); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 58 | |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 59 | private final ArrayList<View> mTempList = new ArrayList<View>(); |
| 60 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 61 | // 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 Meglio | 7e0a372 | 2012-03-27 16:06:25 -0700 | [diff] [blame] | 67 | * @param root Contains focused. Cannot be null. |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 68 | * @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 Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 73 | return findNextFocus(root, focused, null, direction); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 74 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 75 | |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 76 | /** |
| 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 Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 85 | mFocusedRect.set(focusedRect); |
| 86 | return findNextFocus(root, null, mFocusedRect, direction); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 87 | } |
| 88 | |
| 89 | private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 90 | View next = null; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 91 | if (focused != null) { |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 92 | next = findNextUserSpecifiedFocus(root, focused, direction); |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 93 | } |
| 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 Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 103 | } |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 104 | } finally { |
| 105 | focusables.clear(); |
| 106 | } |
| 107 | return next; |
| 108 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 109 | |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 110 | /** |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 111 | * Find the root of the next keyboard navigation cluster after the current one. |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 112 | * @param root The view tree to look inside. Cannot be null |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 113 | * @param currentCluster The starting point of the search. Null means the default cluster |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 114 | * @param direction Direction to look |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 115 | * @return The next cluster, or null if none exists |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 116 | */ |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 117 | public View findNextKeyboardNavigationCluster( |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 118 | @NonNull View root, |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 119 | @Nullable View currentCluster, |
Evan Rosky | 5722331 | 2017-02-08 14:42:45 -0800 | [diff] [blame] | 120 | @View.FocusDirection int direction) { |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 121 | View next = null; |
| 122 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 123 | final ArrayList<View> clusters = mTempList; |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 124 | try { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 125 | clusters.clear(); |
| 126 | root.addKeyboardNavigationClusters(clusters, direction); |
| 127 | if (!clusters.isEmpty()) { |
| 128 | next = findNextKeyboardNavigationCluster( |
| 129 | root, currentCluster, clusters, direction); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 130 | } |
| 131 | } finally { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 132 | clusters.clear(); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 133 | } |
| 134 | return next; |
| 135 | } |
| 136 | |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 137 | private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) { |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 138 | // 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 Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 150 | if (focused != null) { |
Svetoslav Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 151 | if (focusedRect == null) { |
| 152 | focusedRect = mFocusedRect; |
| 153 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 154 | // fill in interesting rect from focused |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 155 | focused.getFocusedRect(focusedRect); |
| 156 | root.offsetDescendantRectToMyCoords(focused, focusedRect); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 157 | } else { |
Svetoslav Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 158 | if (focusedRect == null) { |
| 159 | focusedRect = mFocusedRect; |
| 160 | // make up a rect at top left or bottom right of root |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 161 | switch (direction) { |
Svetoslav Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 162 | case View.FOCUS_RIGHT: |
| 163 | case View.FOCUS_DOWN: |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 164 | setFocusTopLeft(root, focusedRect); |
Svetoslav Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 165 | 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 Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 173 | |
Svetoslav Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 174 | case View.FOCUS_LEFT: |
| 175 | case View.FOCUS_UP: |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 176 | setFocusBottomRight(root, focusedRect); |
Svetoslav Ganov | 951bb42 | 2012-04-30 13:15:21 -0700 | [diff] [blame] | 177 | 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 Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 185 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 186 | } |
| 187 | } |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 188 | |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 189 | switch (direction) { |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 190 | case View.FOCUS_FORWARD: |
| 191 | case View.FOCUS_BACKWARD: |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 192 | return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, |
| 193 | direction); |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 194 | case View.FOCUS_UP: |
| 195 | case View.FOCUS_DOWN: |
| 196 | case View.FOCUS_LEFT: |
| 197 | case View.FOCUS_RIGHT: |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 198 | return findNextFocusInAbsoluteDirection(focusables, root, focused, |
| 199 | focusedRect, direction); |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 200 | default: |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 201 | throw new IllegalArgumentException("Unknown direction: " + direction); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 202 | } |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 203 | } |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 204 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 205 | private View findNextKeyboardNavigationCluster( |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 206 | View root, |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 207 | View currentCluster, |
| 208 | List<View> clusters, |
Evan Rosky | 5722331 | 2017-02-08 14:42:45 -0800 | [diff] [blame] | 209 | @View.FocusDirection int direction) { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 210 | final int count = clusters.size(); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 211 | |
| 212 | switch (direction) { |
| 213 | case View.FOCUS_FORWARD: |
| 214 | case View.FOCUS_DOWN: |
| 215 | case View.FOCUS_RIGHT: |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 216 | return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 217 | case View.FOCUS_BACKWARD: |
| 218 | case View.FOCUS_UP: |
| 219 | case View.FOCUS_LEFT: |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 220 | return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 221 | default: |
| 222 | throw new IllegalArgumentException("Unknown direction: " + direction); |
| 223 | } |
| 224 | } |
| 225 | |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 226 | private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 227 | View focused, Rect focusedRect, int direction) { |
| 228 | try { |
| 229 | // Note: This sort is stable. |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 230 | mUserSpecifiedFocusComparator.setFocusables(focusables); |
| 231 | Collections.sort(focusables, mUserSpecifiedFocusComparator); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 232 | } finally { |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 233 | mUserSpecifiedFocusComparator.recycle(); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 234 | } |
| 235 | |
| 236 | final int count = focusables.size(); |
| 237 | switch (direction) { |
| 238 | case View.FOCUS_FORWARD: |
Fabrice Di Meglio | 3167c88 | 2013-05-07 15:39:55 -0700 | [diff] [blame] | 239 | return getNextFocusable(focused, focusables, count); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 240 | case View.FOCUS_BACKWARD: |
Fabrice Di Meglio | 3167c88 | 2013-05-07 15:39:55 -0700 | [diff] [blame] | 241 | return getPreviousFocusable(focused, focusables, count); |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 242 | } |
| 243 | return focusables.get(count - 1); |
| 244 | } |
| 245 | |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 246 | private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 247 | final int rootBottom = root.getScrollY() + root.getHeight(); |
| 248 | final int rootRight = root.getScrollX() + root.getWidth(); |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 249 | focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 250 | } |
| 251 | |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 252 | private void setFocusTopLeft(ViewGroup root, Rect focusedRect) { |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 253 | final int rootTop = root.getScrollY(); |
| 254 | final int rootLeft = root.getScrollX(); |
Svetoslav Ganov | 76f287e | 2012-04-23 11:02:36 -0700 | [diff] [blame] | 255 | focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 256 | } |
| 257 | |
Svetoslav Ganov | 27e2da7 | 2012-07-02 18:12:00 -0700 | [diff] [blame] | 258 | View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, |
Svetoslav Ganov | 4213804 | 2012-03-20 11:51:39 -0700 | [diff] [blame] | 259 | Rect focusedRect, int direction) { |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 260 | // 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 Dubois | defdb1e | 2010-12-15 11:35:30 +0100 | [diff] [blame] | 286 | // get focus bounds of other view in same coordinate system |
Fabrice Di Meglio | c11f77f | 2012-09-18 15:33:07 -0700 | [diff] [blame] | 287 | focusable.getFocusedRect(mOtherRect); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 288 | 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 Meglio | 7e0a372 | 2012-03-27 16:06:25 -0700 | [diff] [blame] | 298 | private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) { |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 299 | 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 Ganov | e5dfa47d | 2012-05-08 15:58:32 -0700 | [diff] [blame] | 305 | if (!focusables.isEmpty()) { |
| 306 | return focusables.get(0); |
| 307 | } |
| 308 | return null; |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 309 | } |
| 310 | |
Fabrice Di Meglio | 7e0a372 | 2012-03-27 16:06:25 -0700 | [diff] [blame] | 311 | private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) { |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 312 | if (focused != null) { |
| 313 | int position = focusables.indexOf(focused); |
| 314 | if (position > 0) { |
| 315 | return focusables.get(position - 1); |
| 316 | } |
| 317 | } |
Svetoslav Ganov | e5dfa47d | 2012-05-08 15:58:32 -0700 | [diff] [blame] | 318 | if (!focusables.isEmpty()) { |
| 319 | return focusables.get(count - 1); |
| 320 | } |
| 321 | return null; |
Fabrice Di Meglio | 702e8f9 | 2012-03-23 18:09:20 -0700 | [diff] [blame] | 322 | } |
| 323 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 324 | private static View getNextKeyboardNavigationCluster( |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 325 | View root, |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 326 | View currentCluster, |
| 327 | List<View> clusters, |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 328 | int count) { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 329 | 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 Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 334 | } |
| 335 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 336 | final int position = clusters.lastIndexOf(currentCluster); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 337 | if (position >= 0 && position + 1 < count) { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 338 | // Return the next non-default cluster if we can find it. |
| 339 | return clusters.get(position + 1); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 340 | } |
| 341 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 342 | // The current cluster is the last one. The next one is the default one, i.e. the |
| 343 | // root. |
| 344 | return root; |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 345 | } |
| 346 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 347 | private static View getPreviousKeyboardNavigationCluster( |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 348 | View root, |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 349 | View currentCluster, |
| 350 | List<View> clusters, |
Vadim Tryshev | 8957f2d | 2016-12-21 19:22:26 -0800 | [diff] [blame] | 351 | int count) { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 352 | 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 Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 357 | } |
| 358 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 359 | final int position = clusters.indexOf(currentCluster); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 360 | if (position > 0) { |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 361 | // Return the previous non-default cluster if we can find it. |
| 362 | return clusters.get(position - 1); |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 363 | } |
| 364 | |
Vadim Tryshev | b5ced22 | 2017-01-17 19:31:35 -0800 | [diff] [blame] | 365 | // The current cluster is the first one. The previous one is the default one, i.e. |
| 366 | // the root. |
| 367 | return root; |
Vadim Tryshev | 01b0c9e | 2016-11-21 15:25:01 -0800 | [diff] [blame] | 368 | } |
| 369 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 370 | /** |
| 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 Meglio | 7e0a372 | 2012-03-27 16:06:25 -0700 | [diff] [blame] | 490 | * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap? |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 491 | * @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 Meglio | 7e0a372 | 2012-03-27 16:06:25 -0700 | [diff] [blame] | 578 | * edge of the destination rectangle. |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 579 | * @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 Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 701 | |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 702 | private static final boolean isValidId(final int id) { |
| 703 | return id != 0 && id != View.NO_ID; |
| 704 | } |
| 705 | |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 706 | 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 Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 714 | private final Rect mFirstRect = new Rect(); |
| 715 | private final Rect mSecondRect = new Rect(); |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 716 | private ViewGroup mRoot = null; |
Fabrice Di Meglio | 3167c88 | 2013-05-07 15:39:55 -0700 | [diff] [blame] | 717 | private boolean mIsLayoutRtl; |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 718 | |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 719 | public void setIsLayoutRtl(boolean b) { |
| 720 | mIsLayoutRtl = b; |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 721 | } |
| 722 | |
| 723 | public void setRoot(ViewGroup root) { |
| 724 | mRoot = root; |
| 725 | } |
| 726 | |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 727 | 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 Rosky | e466752 | 2017-03-09 20:32:49 +0000 | [diff] [blame] | 735 | 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 Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 751 | } else { |
Evan Rosky | e466752 | 2017-03-09 20:32:49 +0000 | [diff] [blame] | 752 | // 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 Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 756 | } |
| 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 Meglio | 3167c88 | 2013-05-07 15:39:55 -0700 | [diff] [blame] | 780 | } |
| 781 | |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 782 | 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 Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 802 | |
| 803 | for (int i = 0; i < focusables.size(); ++i) { |
| 804 | mOriginalOrdinal.put(focusables.get(i), i); |
| 805 | } |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 806 | } |
| 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 Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 825 | public int compare(View first, View second) { |
| 826 | if (first == second) { |
| 827 | return 0; |
| 828 | } |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 829 | // 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 Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 844 | boolean involvesChain = false; |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 845 | if (firstHead != null) { |
| 846 | first = firstHead; |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 847 | involvesChain = true; |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 848 | } |
| 849 | if (secondHead != null) { |
| 850 | second = secondHead; |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 851 | involvesChain = true; |
George Mount | 140ea62 | 2015-10-27 08:46:44 -0700 | [diff] [blame] | 852 | } |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 853 | |
Evan Rosky | 3b94bf5 | 2017-01-10 17:05:28 -0800 | [diff] [blame] | 854 | if (involvesChain) { |
| 855 | // keep original order between chains |
| 856 | return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1; |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 857 | } else { |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 858 | return 0; |
| 859 | } |
| 860 | } |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 861 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 862 | } |