1 /* 2 * Copyright 2018 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 androidx.recyclerview.widget; 18 19 import static org.hamcrest.CoreMatchers.equalTo; 20 import static org.hamcrest.CoreMatchers.is; 21 import static org.junit.Assert.assertThat; 22 23 import android.content.Context; 24 import android.support.test.InstrumentationRegistry; 25 import android.support.test.filters.LargeTest; 26 import android.support.test.filters.SdkSuppress; 27 import android.support.test.rule.ActivityTestRule; 28 import android.support.test.runner.AndroidJUnit4; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.widget.TextView; 32 33 import org.junit.Rule; 34 import org.junit.Test; 35 import org.junit.runner.RunWith; 36 37 @RunWith(AndroidJUnit4.class) 38 @LargeTest 39 public class RecyclerViewFocusTest { 40 41 private static final int RV_HEIGHT_WIDTH = 200; 42 private static final int ITEM_HEIGHT_WIDTH = 100; 43 44 private RecyclerView mRecyclerView; 45 private TestLinearLayoutManager mTestLinearLayoutManager; 46 private TestContentView mTestContentView; 47 48 @Rule 49 public ActivityTestRule<TestContentViewActivity> mActivityRule = 50 new ActivityTestRule<>(TestContentViewActivity.class); 51 52 @Test 53 public void focusSearch_layoutInterceptsAndReturnsNotNull_valueReturned() throws Throwable { 54 setupRecyclerView(true, RecyclerView.VERTICAL, true); 55 View expectedView = new View(mActivityRule.getActivity()); 56 View currentlyFocusedView = mRecyclerView.getChildAt(0); 57 mTestLinearLayoutManager.mOnInterceptFocusSearchReturnValue = expectedView; 58 59 View actualView = mRecyclerView.focusSearch(currentlyFocusedView, View.FOCUS_FORWARD); 60 61 assertThat(actualView, is(equalTo(expectedView))); 62 } 63 64 @Test 65 public void focusSearch_noAdapter_onFocusSearchFailedNotCalled() throws Throwable { 66 setupRecyclerView(false, RecyclerView.VERTICAL, true); 67 View currentlyFocusedView = mRecyclerView.getChildAt(1); 68 69 mRecyclerView.focusSearch(currentlyFocusedView, View.FOCUS_FORWARD); 70 71 assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false)); 72 } 73 74 @Test 75 public void focusSearch_layoutFrozen_onFocusSearchFailedNotCalled() throws Throwable { 76 setupRecyclerView(true, RecyclerView.VERTICAL, true); 77 mRecyclerView.setLayoutFrozen(true); 78 View currentlyFocusedView = mRecyclerView.getChildAt(1); 79 80 mRecyclerView.focusSearch(currentlyFocusedView, View.FOCUS_FORWARD); 81 82 assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false)); 83 } 84 85 @Test 86 public void focusSearch_focusedViewNull_onFocusSearchFailedNotCalled() throws Throwable { 87 setupRecyclerView(true, RecyclerView.VERTICAL, true); 88 mRecyclerView.focusSearch(null, View.FOCUS_FORWARD); 89 assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false)); 90 } 91 92 /* 93 Failures, null is returned 94 Tests to verify when onFocusSearchFailed is called. 95 */ 96 97 @Test 98 public void focusSearch_verticalAndHasChildInDirection_findsCorrectChild() throws Throwable { 99 setupRecyclerView(true, RecyclerView.VERTICAL, true); 100 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_FORWARD, 0, 1); 101 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_BACKWARD, 1, 0); 102 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_DOWN, 0, 1); 103 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_UP, 1, 0); 104 } 105 106 @Test 107 public void focusSearch_horizontalAndHasChildInDirection_findsCorrectChild() throws Throwable { 108 setupRecyclerView(true, RecyclerView.HORIZONTAL, true); 109 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_FORWARD, 0, 1); 110 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_BACKWARD, 1, 0); 111 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_RIGHT, 0, 1); 112 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_LEFT, 1, 0); 113 } 114 115 @Test 116 @SdkSuppress(minSdkVersion = 17) 117 public void focusSearch_horizontalRtlAndHasChildInDirection_findsCorrectChild() 118 throws Throwable { 119 setupRecyclerView(true, RecyclerView.HORIZONTAL, false); 120 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_FORWARD, 0, 1); 121 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_BACKWARD, 1, 0); 122 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_RIGHT, 1, 0); 123 focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_LEFT, 0, 1); 124 } 125 126 @Test 127 public void focusSearch_verticalAndHasChildInDirection_doesNotCallOnFocusSearchFailed() 128 throws Throwable { 129 setupRecyclerView(true, RecyclerView.VERTICAL, true); 130 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 131 View.FOCUS_FORWARD, 0); 132 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 133 View.FOCUS_BACKWARD, 1); 134 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 135 View.FOCUS_DOWN, 0); 136 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 137 View.FOCUS_UP, 1); 138 } 139 140 @Test 141 public void focusSearch_horizontalAndHasChildInDirection_doesNotCallOnFocusSearchFailed() 142 throws Throwable { 143 setupRecyclerView(true, RecyclerView.HORIZONTAL, true); 144 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 145 View.FOCUS_FORWARD, 0); 146 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 147 View.FOCUS_BACKWARD, 1); 148 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 149 View.FOCUS_RIGHT, 0); 150 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 151 View.FOCUS_LEFT, 1); 152 } 153 154 @Test 155 @SdkSuppress(minSdkVersion = 17) 156 public void focusSearch_horizontalRtlAndHasChildInDirection_doesNotCallOnFocusSearchFailed() 157 throws Throwable { 158 setupRecyclerView(true, RecyclerView.HORIZONTAL, false); 159 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 160 View.FOCUS_FORWARD, 0); 161 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 162 View.FOCUS_BACKWARD, 1); 163 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 164 View.FOCUS_RIGHT, 1); 165 focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 166 View.FOCUS_LEFT, 0); 167 } 168 169 @Test 170 public void focusSearch_verticalAndDoesNotHaveChildInDirection_callsOnFocusSearchFailed() 171 throws Throwable { 172 setupRecyclerView(true, RecyclerView.VERTICAL, true); 173 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_FORWARD, 1); 174 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_BACKWARD, 0); 175 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_DOWN, 1); 176 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_UP, 0); 177 } 178 179 @Test 180 public void focusSearch_horizontalAndDoesNotHaveChildInDirection_callsOnFocusSearchFailed() 181 throws Throwable { 182 setupRecyclerView(true, RecyclerView.HORIZONTAL, true); 183 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_FORWARD, 1); 184 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_BACKWARD, 0); 185 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_RIGHT, 1); 186 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_LEFT, 0); 187 } 188 189 @Test 190 @SdkSuppress(minSdkVersion = 17) 191 public void focusSearch_horizontalRtlAndDoesNotHaveChildInDirection_callsOnFocusSearchFailed() 192 throws Throwable { 193 setupRecyclerView(true, RecyclerView.HORIZONTAL, false); 194 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_FORWARD, 1); 195 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_BACKWARD, 0); 196 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_RIGHT, 0); 197 focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_LEFT, 1); 198 } 199 200 private void focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(int direction, 201 int startingChild, int expectedChild) { 202 View currentlyFocusedView = mRecyclerView.getChildAt(startingChild); 203 View expectedResult = mRecyclerView.getChildAt(expectedChild); 204 205 View actualResult = mRecyclerView.focusSearch(currentlyFocusedView, direction); 206 207 assertThat(actualResult, is(equalTo(expectedResult))); 208 } 209 210 private void focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled( 211 int direction, int startingChild) { 212 mTestLinearLayoutManager.mOnFocusSearchFailedCalled = false; 213 View currentlyFocusedView = mRecyclerView.getChildAt(startingChild); 214 215 mRecyclerView.focusSearch(currentlyFocusedView, direction); 216 217 assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false)); 218 } 219 220 private void focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(int direction, 221 int startingChild) { 222 mTestLinearLayoutManager.mOnFocusSearchFailedCalled = false; 223 View currentlyFocusedView = mRecyclerView.getChildAt(startingChild); 224 mRecyclerView.focusSearch(currentlyFocusedView, direction); 225 assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(true)); 226 } 227 228 private void setupRecyclerView(final boolean hasAdapter, int orientation, boolean ltr) 229 throws Throwable { 230 final TestContentViewActivity testContentViewActivity = mActivityRule.getActivity(); 231 mTestContentView = testContentViewActivity.getContentView(); 232 233 mTestLinearLayoutManager = new TestLinearLayoutManager(testContentViewActivity, 234 orientation, false); 235 236 mRecyclerView = new RecyclerView(InstrumentationRegistry.getContext()); 237 if (!ltr) { 238 mRecyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL); 239 } 240 mRecyclerView.setBackgroundColor(0xFFFF0000); 241 mRecyclerView.setLayoutParams( 242 new TestContentView.LayoutParams(RV_HEIGHT_WIDTH, RV_HEIGHT_WIDTH)); 243 mRecyclerView.setLayoutManager(mTestLinearLayoutManager); 244 245 if (hasAdapter) { 246 mRecyclerView.setAdapter( 247 new TestAdapter(100, ITEM_HEIGHT_WIDTH, ITEM_HEIGHT_WIDTH)); 248 } 249 250 mTestContentView.expectLayouts(1); 251 mActivityRule.runOnUiThread(new Runnable() { 252 @Override 253 public void run() { 254 mTestContentView.addView(mRecyclerView); 255 } 256 }); 257 mTestContentView.awaitLayouts(2); 258 } 259 260 private class TestLinearLayoutManager extends LinearLayoutManager { 261 262 boolean mOnFocusSearchFailedCalled = false; 263 View mOnInterceptFocusSearchReturnValue; 264 View mViewToReturnFromonFocusSearchFailed; 265 266 TestLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 267 super(context, orientation, reverseLayout); 268 } 269 270 @Override 271 public View onInterceptFocusSearch(View focused, int direction) { 272 return mOnInterceptFocusSearchReturnValue; 273 } 274 275 @Override 276 public View onFocusSearchFailed(View focused, int focusDirection, 277 RecyclerView.Recycler recycler, RecyclerView.State state) { 278 mOnFocusSearchFailedCalled = true; 279 return mViewToReturnFromonFocusSearchFailed; 280 } 281 } 282 283 private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> { 284 285 private int mItemCount; 286 private int mItemLayoutWidth; 287 private int mItemLayoutHeight; 288 289 TestAdapter(int itemCount, int itemLayoutWidth, int itemLayoutHeight) { 290 mItemCount = itemCount; 291 mItemLayoutWidth = itemLayoutWidth; 292 mItemLayoutHeight = itemLayoutHeight; 293 } 294 295 @Override 296 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 297 TextView textView = new TextView(parent.getContext()); 298 textView.setLayoutParams( 299 new ViewGroup.LayoutParams(mItemLayoutWidth, mItemLayoutHeight)); 300 textView.setFocusableInTouchMode(true); 301 return new TestViewHolder(textView); 302 } 303 304 @Override 305 public void onBindViewHolder(TestViewHolder holder, int position) { 306 ((TextView) holder.itemView).setText("Position: " + position); 307 } 308 309 @Override 310 public int getItemCount() { 311 return mItemCount; 312 } 313 } 314 315 private class TestViewHolder extends RecyclerView.ViewHolder { 316 317 TestViewHolder(View itemView) { 318 super(itemView); 319 } 320 } 321 } 322