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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21 
22 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
23 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
24 
25 import static com.google.common.truth.Truth.assertThat;
26 
27 import static org.hamcrest.CoreMatchers.is;
28 import static org.junit.Assert.assertEquals;
29 import static org.junit.Assert.assertFalse;
30 import static org.junit.Assert.assertNotNull;
31 import static org.junit.Assert.assertNull;
32 import static org.junit.Assert.assertSame;
33 import static org.junit.Assert.assertThat;
34 import static org.junit.Assert.assertTrue;
35 
36 import android.graphics.Color;
37 import android.graphics.drawable.ColorDrawable;
38 import android.graphics.drawable.StateListDrawable;
39 import android.os.Build;
40 import android.os.Bundle;
41 import android.util.Log;
42 import android.util.StateSet;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityNodeInfo;
47 import android.widget.TextView;
48 
49 import androidx.core.view.AccessibilityDelegateCompat;
50 import androidx.core.view.ViewCompat;
51 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
52 import androidx.test.filters.LargeTest;
53 import androidx.test.filters.SdkSuppress;
54 
55 import org.jspecify.annotations.NonNull;
56 import org.junit.Test;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 import java.util.concurrent.CountDownLatch;
61 import java.util.concurrent.TimeUnit;
62 import java.util.concurrent.atomic.AtomicInteger;
63 
64 /**
65  * Includes tests for {@link LinearLayoutManager}.
66  * <p>
67  * Since most UI tests are not practical, these tests are focused on internal data representation
68  * and stability of LinearLayoutManager in response to different events (state change, scrolling
69  * etc) where it is very hard to do manual testing.
70  */
71 @LargeTest
72 public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest {
73 
74     /**
75      * Tests that the LinearLayoutManager retains the focused element after multiple measure
76      * calls to the RecyclerView.  There was a bug where the focused view was lost when the soft
77      * keyboard opened.  This test simulates the measure/layout events triggered by the opening
78      * of the soft keyboard by making two calls to measure.  A simulation was done because using
79      * the soft keyboard in the test caused many issues on API levels 15, 17 and 19.
80      */
81     @Test
focusedChildStaysInViewWhenRecyclerViewShrinks()82     public void focusedChildStaysInViewWhenRecyclerViewShrinks() throws Throwable {
83 
84         // Arrange.
85 
86         final RecyclerView recyclerView = inflateWrappedRV();
87         ViewGroup.LayoutParams lp = recyclerView.getLayoutParams();
88         lp.height = WRAP_CONTENT;
89         lp.width = MATCH_PARENT;
90         recyclerView.setHasFixedSize(true);
91 
92         final FocusableAdapter focusableAdapter =
93                 new FocusableAdapter(50);
94         recyclerView.setAdapter(focusableAdapter);
95 
96         mLayoutManager = new WrappedLinearLayoutManager(getActivity(), VERTICAL, false);
97         recyclerView.setLayoutManager(mLayoutManager);
98 
99         mLayoutManager.expectLayouts(1);
100         mActivityRule.runOnUiThread(new Runnable() {
101             @Override
102             public void run() {
103                 getActivity().getContainer().addView(recyclerView);
104             }
105         });
106         mLayoutManager.waitForLayout(3);
107 
108         int width = recyclerView.getWidth();
109         int height = recyclerView.getHeight();
110         final int widthMeasureSpec =
111                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
112         final int fullHeightMeasureSpec =
113                 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
114         // "MinusOne" so that a measure call will appropriately trigger onMeasure after RecyclerView
115         // was previously laid out with the full height version.
116         final int fullHeightMinusOneMeasureSpec =
117                 View.MeasureSpec.makeMeasureSpec(height - 1, View.MeasureSpec.AT_MOST);
118         final int halfHeightMeasureSpec =
119                 View.MeasureSpec.makeMeasureSpec(height / 2, View.MeasureSpec.AT_MOST);
120 
121         // Act 1.
122 
123         View toFocus = findLastFullyVisibleChild(recyclerView);
124         int focusIndex = recyclerView.getChildAdapterPosition(toFocus);
125 
126         requestFocus(toFocus, false);
127 
128         mLayoutManager.expectLayouts(1);
129         mActivityRule.runOnUiThread(new Runnable() {
130             @Override
131             public void run() {
132                 recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
133                 recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
134                 recyclerView.layout(
135                         0,
136                         0,
137                         recyclerView.getMeasuredWidth(),
138                         recyclerView.getMeasuredHeight());
139             }
140         });
141         mLayoutManager.waitForLayout(3);
142 
143         // Verify 1.
144 
145         assertThat("Child at position " + focusIndex + " should be focused",
146                 toFocus.hasFocus(), is(true));
147         // Testing for partial visibility instead of full visibility since TextView calls
148         // requestRectangleOnScreen (inside bringPointIntoView) for the focused view with a rect
149         // containing the content area. This rect is guaranteed to be fully visible whereas a
150         // portion of TextView could be out of bounds.
151         assertThat("Child view at adapter pos " + focusIndex + " should be fully visible.",
152                 isViewPartiallyInBound(recyclerView, toFocus), is(true));
153 
154         // Act 2.
155 
156         mLayoutManager.expectLayouts(1);
157         mActivityRule.runOnUiThread(new Runnable() {
158             @Override
159             public void run() {
160                 recyclerView.measure(widthMeasureSpec, fullHeightMeasureSpec);
161                 recyclerView.layout(
162                         0,
163                         0,
164                         recyclerView.getMeasuredWidth(),
165                         recyclerView.getMeasuredHeight());
166             }
167         });
168         mLayoutManager.waitForLayout(3);
169 
170         // Verify 2.
171 
172         assertThat("Child at position " + focusIndex + " should be focused",
173                 toFocus.hasFocus(), is(true));
174         assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
175                 isViewPartiallyInBound(recyclerView, toFocus));
176 
177         // Act 3.
178 
179         // Now focus on the first fully visible EditText.
180         toFocus = findFirstFullyVisibleChild(recyclerView);
181         focusIndex = recyclerView.getChildAdapterPosition(toFocus);
182 
183         requestFocus(toFocus, false);
184 
185         mLayoutManager.expectLayouts(1);
186         mActivityRule.runOnUiThread(new Runnable() {
187             @Override
188             public void run() {
189                 recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
190                 recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
191                 recyclerView.layout(
192                         0,
193                         0,
194                         recyclerView.getMeasuredWidth(),
195                         recyclerView.getMeasuredHeight());
196             }
197         });
198         mLayoutManager.waitForLayout(3);
199 
200         // Assert 3.
201 
202         assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
203                 isViewPartiallyInBound(recyclerView, toFocus));
204     }
205 
206     @Test
topUnfocusableViewsVisibility()207     public void topUnfocusableViewsVisibility() throws Throwable {
208         // The maximum number of child views that can be visible at any time.
209         final int visibleChildCount = 5;
210         final int consecutiveFocusablesCount = 2;
211         final int consecutiveUnFocusablesCount = 18;
212         final TestAdapter adapter = new TestAdapter(
213                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
214             RecyclerView mAttachedRv;
215 
216             @Override
217             @SuppressWarnings("deprecated") // using this for kitkat tests
218             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
219                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
220                 // Good to have colors for debugging
221                 StateListDrawable stl = new StateListDrawable();
222                 stl.addState(new int[]{android.R.attr.state_focused},
223                         new ColorDrawable(Color.RED));
224                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
225                 testViewHolder.itemView.setBackgroundDrawable(stl);
226                 return testViewHolder;
227             }
228 
229             @Override
230             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
231                 mAttachedRv = recyclerView;
232             }
233 
234             @Override
235             public void onBindViewHolder(@NonNull TestViewHolder holder,
236                     int position) {
237                 super.onBindViewHolder(holder, position);
238                 if (position < consecutiveFocusablesCount) {
239                     holder.itemView.setFocusable(true);
240                     holder.itemView.setFocusableInTouchMode(true);
241                 } else {
242                     holder.itemView.setFocusable(false);
243                     holder.itemView.setFocusableInTouchMode(false);
244                 }
245                 // This height ensures that some portion of #visibleChildCount'th child is
246                 // off-bounds, creating more interesting test scenario.
247                 holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
248                         + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
249             }
250         };
251         setupByConfig(new Config(VERTICAL, false, false).adapter(adapter).reverseLayout(true),
252                 false);
253         waitForFirstLayout();
254 
255         // adapter position of the currently focused item.
256         int focusIndex = 0;
257         View newFocused = mRecyclerView.getChildAt(focusIndex);
258         requestFocus(newFocused, true);
259         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
260                 focusIndex);
261         assertThat("Child at position " + focusIndex + " should be focused",
262                 toFocus.itemView.hasFocus(), is(true));
263 
264         // adapter position of the item (whether focusable or not) that just becomes fully
265         // visible after focusSearch.
266         int visibleIndex = 0;
267         // The VH of the above adapter position
268         RecyclerView.ViewHolder toVisible = null;
269 
270         // Navigate up through the focusable and unfocusable chunks. The focusable items should
271         // become focused one by one until hitting the last focusable item, at which point,
272         // unfocusable items should become visible on the screen until the currently focused item
273         // stays on the screen.
274         for (int i = 0; i < adapter.getItemCount(); i++) {
275             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_UP, true);
276             // adapter position of the currently focused item.
277             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
278             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
279             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
280                     (visibleIndex + 1));
281             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
282 
283             assertThat("Child at position " + focusIndex + " should be focused",
284                     toFocus.itemView.hasFocus(), is(true));
285             assertTrue("Focused child should be at least partially visible.",
286                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
287             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
288                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
289         }
290     }
291 
292     @Test
bottomUnfocusableViewsVisibility()293     public void bottomUnfocusableViewsVisibility() throws Throwable {
294         // The maximum number of child views that can be visible at any time.
295         final int visibleChildCount = 5;
296         final int consecutiveFocusablesCount = 2;
297         final int consecutiveUnFocusablesCount = 18;
298         final TestAdapter adapter = new TestAdapter(
299                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
300             RecyclerView mAttachedRv;
301 
302             @Override
303             @SuppressWarnings("deprecated") // using this for kitkat tests
304             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
305                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
306                 // Good to have colors for debugging
307                 StateListDrawable stl = new StateListDrawable();
308                 stl.addState(new int[]{android.R.attr.state_focused},
309                         new ColorDrawable(Color.RED));
310                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
311                 testViewHolder.itemView.setBackgroundDrawable(stl);
312                 return testViewHolder;
313             }
314 
315             @Override
316             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
317                 mAttachedRv = recyclerView;
318             }
319 
320             @Override
321             public void onBindViewHolder(@NonNull TestViewHolder holder,
322                     int position) {
323                 super.onBindViewHolder(holder, position);
324                 if (position < consecutiveFocusablesCount) {
325                     holder.itemView.setFocusable(true);
326                     holder.itemView.setFocusableInTouchMode(true);
327                 } else {
328                     holder.itemView.setFocusable(false);
329                     holder.itemView.setFocusableInTouchMode(false);
330                 }
331                 // This height ensures that some portion of #visibleChildCount'th child is
332                 // off-bounds, creating more interesting test scenario.
333                 holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
334                         + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
335             }
336         };
337         setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
338         waitForFirstLayout();
339 
340         // adapter position of the currently focused item.
341         int focusIndex = 0;
342         View newFocused = mRecyclerView.getChildAt(focusIndex);
343         requestFocus(newFocused, true);
344         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
345                 focusIndex);
346         assertThat("Child at position " + focusIndex + " should be focused",
347                 toFocus.itemView.hasFocus(), is(true));
348 
349         // adapter position of the item (whether focusable or not) that just becomes fully
350         // visible after focusSearch.
351         int visibleIndex = 0;
352         // The VH of the above adapter position
353         RecyclerView.ViewHolder toVisible = null;
354 
355         // Navigate down through the focusable and unfocusable chunks. The focusable items should
356         // become focused one by one until hitting the last focusable item, at which point,
357         // unfocusable items should become visible on the screen until the currently focused item
358         // stays on the screen.
359         for (int i = 0; i < adapter.getItemCount(); i++) {
360             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
361             // adapter position of the currently focused item.
362             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
363             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
364             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
365                     (visibleIndex + 1));
366             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
367 
368             assertThat("Child at position " + focusIndex + " should be focused",
369                     toFocus.itemView.hasFocus(), is(true));
370             assertTrue("Focused child should be at least partially visible.",
371                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
372             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
373                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
374         }
375     }
376 
377     @Test
leftUnfocusableViewsVisibility()378     public void leftUnfocusableViewsVisibility() throws Throwable {
379         // The maximum number of child views that can be visible at any time.
380         final int visibleChildCount = 5;
381         final int consecutiveFocusablesCount = 2;
382         final int consecutiveUnFocusablesCount = 18;
383         final int childWidth = 250;
384         final int childHeight = 1000;
385         // Parent width is 1 more than 4 times child width, so when focusable child is 1 pixel on
386         // screen 4 non-focusable children can fit on screen.
387         final int parentWidth = childWidth * 4 + 1;
388         final int parentHeight = childHeight;
389         final TestAdapter adapter = new TestAdapter(
390                 consecutiveFocusablesCount + consecutiveUnFocusablesCount,
391                 new RecyclerView.LayoutParams(childWidth, childHeight)) {
392             RecyclerView mAttachedRv;
393 
394             @Override
395             @SuppressWarnings("deprecated") // using this for kitkat tests
396             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
397                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
398                 // Good to have colors for debugging
399                 StateListDrawable stl = new StateListDrawable();
400                 stl.addState(new int[]{android.R.attr.state_focused},
401                         new ColorDrawable(Color.RED));
402                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
403                 testViewHolder.itemView.setBackgroundDrawable(stl);
404                 return testViewHolder;
405             }
406 
407             @Override
408             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
409                 mAttachedRv = recyclerView;
410             }
411 
412             @Override
413             public void onBindViewHolder(@NonNull TestViewHolder holder,
414                     int position) {
415                 super.onBindViewHolder(holder, position);
416                 if (position < consecutiveFocusablesCount) {
417                     holder.itemView.setFocusable(true);
418                     holder.itemView.setFocusableInTouchMode(true);
419                 } else {
420                     holder.itemView.setFocusable(false);
421                     holder.itemView.setFocusableInTouchMode(false);
422                 }
423                 // This width ensures that some portion of #visibleChildCount'th child is
424                 // off-bounds, creating more interesting test scenario.
425                 holder.itemView.setMinimumWidth((mAttachedRv.getWidth()
426                         + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount);
427             }
428         };
429         setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter).reverseLayout(true),
430                 false,
431                 null,
432                 new RecyclerView.LayoutParams(parentWidth, parentHeight));
433         waitForFirstLayout();
434 
435         // adapter position of the currently focused item.
436         int focusIndex = 0;
437         View newFocused = mRecyclerView.getChildAt(focusIndex);
438         requestFocus(newFocused, true);
439         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
440                 focusIndex);
441         assertThat("Child at position " + focusIndex + " should be focused",
442                 toFocus.itemView.hasFocus(), is(true));
443 
444         // adapter position of the item (whether focusable or not) that just becomes fully
445         // visible after focusSearch.
446         int visibleIndex = 0;
447         // The VH of the above adapter position
448         RecyclerView.ViewHolder toVisible = null;
449 
450         // Navigate left through the focusable and unfocusable chunks. The focusable items should
451         // become focused one by one until hitting the last focusable item, at which point,
452         // unfocusable items should become visible on the screen until the currently focused item
453         // stays on the screen.
454         for (int i = 0; i < adapter.getItemCount(); i++) {
455             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
456             // adapter position of the currently focused item.
457             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
458             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
459             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
460                     (visibleIndex + 1));
461             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
462 
463             assertThat("Child at position " + focusIndex + " should be focused",
464                     toFocus.itemView.hasFocus(), is(true));
465             assertTrue("Focused child should be at least partially visible.",
466                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
467             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
468                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
469         }
470     }
471 
472     @Test
rightUnfocusableViewsVisibility()473     public void rightUnfocusableViewsVisibility() throws Throwable {
474         // The maximum number of child views that can be visible at any time.
475         final int visibleChildCount = 5;
476         final int consecutiveFocusablesCount = 2;
477         final int consecutiveUnFocusablesCount = 18;
478         final int childWidth = 250;
479         final int childHeight = 1000;
480         // Parent width is 1 more than 4 times child width, so when focusable child is 1 pixel on
481         // screen 4 non-focusable children can fit on screen.
482         final int parentWidth = childWidth * 4 + 1;
483         final int parentHeight = childHeight;
484         final TestAdapter adapter = new TestAdapter(
485                 consecutiveFocusablesCount + consecutiveUnFocusablesCount,
486                 new RecyclerView.LayoutParams(childWidth, childHeight)) {
487             RecyclerView mAttachedRv;
488 
489             @Override
490             @SuppressWarnings("deprecated") // using this for kitkat tests
491             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
492                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
493                 // Good to have colors for debugging
494                 StateListDrawable stl = new StateListDrawable();
495                 stl.addState(new int[]{android.R.attr.state_focused},
496                         new ColorDrawable(Color.RED));
497                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
498                 testViewHolder.itemView.setBackgroundDrawable(stl);
499                 return testViewHolder;
500             }
501 
502             @Override
503             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
504                 mAttachedRv = recyclerView;
505             }
506 
507             @Override
508             public void onBindViewHolder(@NonNull TestViewHolder holder,
509                     int position) {
510                 super.onBindViewHolder(holder, position);
511                 if (position < consecutiveFocusablesCount) {
512                     holder.itemView.setFocusable(true);
513                     holder.itemView.setFocusableInTouchMode(true);
514                 } else {
515                     holder.itemView.setFocusable(false);
516                     holder.itemView.setFocusableInTouchMode(false);
517                 }
518                 // This width ensures that some portion of #visibleChildCount'th child is
519                 // off-bounds, creating more interesting test scenario.
520                 holder.itemView.setMinimumWidth((mAttachedRv.getWidth()
521                         + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount);
522             }
523         };
524         setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter),
525                 false,
526                 null,
527                 new RecyclerView.LayoutParams(parentWidth, parentHeight));
528         waitForFirstLayout();
529 
530         // adapter position of the currently focused item.
531         int focusIndex = 0;
532         View newFocused = mRecyclerView.getChildAt(focusIndex);
533         requestFocus(newFocused, true);
534         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
535                 focusIndex);
536         assertThat("Child at position " + focusIndex + " should be focused",
537                 toFocus.itemView.hasFocus(), is(true));
538 
539         // adapter position of the item (whether focusable or not) that just becomes fully
540         // visible after focusSearch.
541         int visibleIndex = 0;
542         // The VH of the above adapter position
543         RecyclerView.ViewHolder toVisible = null;
544 
545         // Navigate right through the focusable and unfocusable chunks. The focusable items should
546         // become focused one by one until hitting the last focusable item, at which point,
547         // unfocusable items should become visible on the screen until the currently focused item
548         // stays on the screen.
549         for (int i = 0; i < adapter.getItemCount(); i++) {
550             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
551             // adapter position of the currently focused item.
552             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
553             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
554             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
555                     (visibleIndex + 1));
556             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
557 
558             assertThat("Child at position " + focusIndex + " should be focused",
559                     toFocus.itemView.hasFocus(), is(true));
560             assertTrue("Focused child should be at least partially visible.",
561                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
562             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
563                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
564         }
565     }
566 
567     @Test
unfocusableScrollingWhenFocusCleared()568     public void unfocusableScrollingWhenFocusCleared() throws Throwable {
569         // The maximum number of child views that can be visible at any time.
570         final int visibleChildCount = 5;
571         final int consecutiveFocusablesCount = 2;
572         final int consecutiveUnFocusablesCount = 18;
573         final TestAdapter adapter = new TestAdapter(
574                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
575             RecyclerView mAttachedRv;
576 
577             @Override
578             @SuppressWarnings("deprecated") // using this for kitkat tests
579             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
580                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
581                 // Good to have colors for debugging
582                 StateListDrawable stl = new StateListDrawable();
583                 stl.addState(new int[]{android.R.attr.state_focused},
584                         new ColorDrawable(Color.RED));
585                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
586                 testViewHolder.itemView.setBackgroundDrawable(stl);
587                 return testViewHolder;
588             }
589 
590             @Override
591             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
592                 mAttachedRv = recyclerView;
593             }
594 
595             @Override
596             public void onBindViewHolder(@NonNull TestViewHolder holder,
597                     int position) {
598                 super.onBindViewHolder(holder, position);
599                 if (position < consecutiveFocusablesCount) {
600                     holder.itemView.setFocusable(true);
601                     holder.itemView.setFocusableInTouchMode(true);
602                 } else {
603                     holder.itemView.setFocusable(false);
604                     holder.itemView.setFocusableInTouchMode(false);
605                 }
606                 // This height ensures that some portion of #visibleChildCount'th child is
607                 // off-bounds, creating more interesting test scenario.
608                 holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
609                         + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
610             }
611         };
612         setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
613         waitForFirstLayout();
614 
615         // adapter position of the currently focused item.
616         int focusIndex = 0;
617         View newFocused = mRecyclerView.getChildAt(focusIndex);
618         requestFocus(newFocused, true);
619         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
620                 focusIndex);
621         assertThat("Child at position " + focusIndex + " should be focused",
622                 toFocus.itemView.hasFocus(), is(true));
623 
624         final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
625         focusIndex++;
626         assertThat("Child at position " + focusIndex + " should be focused",
627                 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
628                 is(true));
629         final CountDownLatch focusLatch = new CountDownLatch(1);
630         mActivityRule.runOnUiThread(new Runnable() {
631             @Override
632             public void run() {
633                 nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){
634                     @Override
635                     public void onFocusChange(View v, boolean hasFocus) {
636                         assertNull("Focus just got cleared and no children should be holding"
637                                 + " focus now.", mRecyclerView.getFocusedChild());
638                         try {
639                             // Calling focusSearch should be a no-op here since even though there
640                             // are unfocusable views down to scroll to, none of RV's children hold
641                             // focus at this stage.
642                             View focusedChild  = focusSearch(v, View.FOCUS_DOWN, true);
643                             assertNull("Calling focusSearch should be no-op when no children hold"
644                                     + "focus", focusedChild);
645                             // No scrolling should have happened, so any unfocusables that were
646                             // invisible should still be invisible.
647                             RecyclerView.ViewHolder unforcusablePartiallyVisibleChild =
648                                     mRecyclerView.findViewHolderForAdapterPosition(
649                                             visibleChildCount - 1);
650                             assertFalse("Child view at adapter pos " + (visibleChildCount - 1)
651                                             + " should not be fully visible.",
652                                     isViewFullyInBound(mRecyclerView,
653                                             unforcusablePartiallyVisibleChild.itemView));
654                         } catch (Throwable t) {
655                             postExceptionToInstrumentation(t);
656                         }
657                     }
658                 });
659                 nextView.clearFocus();
660                 focusLatch.countDown();
661             }
662         });
663         assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
664         assertThat("Child at position " + focusIndex + " should no longer be focused",
665                 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
666                 is(false));
667     }
668 
669     @Test
removeAnchorItem()670     public void removeAnchorItem() throws Throwable {
671         removeAnchorItemTest(
672                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(
673                         false), 100, 0);
674     }
675 
676     @Test
removeAnchorItemReverse()677     public void removeAnchorItemReverse() throws Throwable {
678         removeAnchorItemTest(
679                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100,
680                 0);
681     }
682 
683     @Test
removeAnchorItemStackFromEnd()684     public void removeAnchorItemStackFromEnd() throws Throwable {
685         removeAnchorItemTest(
686                 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100,
687                 99);
688     }
689 
690     @Test
removeAnchorItemStackFromEndAndReverse()691     public void removeAnchorItemStackFromEndAndReverse() throws Throwable {
692         removeAnchorItemTest(
693                 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100,
694                 99);
695     }
696 
697     @Test
removeAnchorItemHorizontal()698     public void removeAnchorItemHorizontal() throws Throwable {
699         removeAnchorItemTest(
700                 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(
701                         false), 100, 0);
702     }
703 
704     @Test
removeAnchorItemReverseHorizontal()705     public void removeAnchorItemReverseHorizontal() throws Throwable {
706         removeAnchorItemTest(
707                 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true),
708                 100, 0);
709     }
710 
711     @Test
removeAnchorItemStackFromEndHorizontal()712     public void removeAnchorItemStackFromEndHorizontal() throws Throwable {
713         removeAnchorItemTest(
714                 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false),
715                 100, 99);
716     }
717 
718     @Test
removeAnchorItemStackFromEndAndReverseHorizontal()719     public void removeAnchorItemStackFromEndAndReverseHorizontal() throws Throwable {
720         removeAnchorItemTest(
721                 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100,
722                 99);
723     }
724 
725     /**
726      * This tests a regression where predictive animations were not working as expected when the
727      * first item is removed and there aren't any more items to add from that direction.
728      * First item refers to the default anchor item.
729      */
removeAnchorItemTest(final Config config, int adapterSize, final int removePos)730     public void removeAnchorItemTest(final Config config, int adapterSize,
731             final int removePos) throws Throwable {
732         config.adapter(new TestAdapter(adapterSize) {
733             @Override
734             public void onBindViewHolder(@NonNull TestViewHolder holder,
735                     int position) {
736                 super.onBindViewHolder(holder, position);
737                 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
738                 if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
739                     lp = new ViewGroup.MarginLayoutParams(0, 0);
740                     holder.itemView.setLayoutParams(lp);
741                 }
742                 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
743                 final int maxSize;
744                 if (config.mOrientation == HORIZONTAL) {
745                     maxSize = mRecyclerView.getWidth();
746                     mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
747                 } else {
748                     maxSize = mRecyclerView.getHeight();
749                     mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
750                 }
751 
752                 final int desiredSize;
753                 if (position == removePos) {
754                     // make it large
755                     desiredSize = maxSize / 4;
756                 } else {
757                     // make it small
758                     desiredSize = maxSize / 8;
759                 }
760                 if (config.mOrientation == HORIZONTAL) {
761                     mlp.width = desiredSize;
762                 } else {
763                     mlp.height = desiredSize;
764                 }
765             }
766         });
767         setupByConfig(config, true);
768         final int childCount = mLayoutManager.getChildCount();
769         RecyclerView.ViewHolder toBeRemoved = null;
770         List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
771         for (int i = 0; i < childCount; i++) {
772             View child = mLayoutManager.getChildAt(i);
773             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
774             if (holder.getAbsoluteAdapterPosition() == removePos) {
775                 toBeRemoved = holder;
776             } else {
777                 toBeMoved.add(holder);
778             }
779         }
780         assertNotNull("Assumption check", toBeRemoved);
781         assertEquals("Assumption check", childCount - 1, toBeMoved.size());
782         LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
783         mRecyclerView.setItemAnimator(loggingItemAnimator);
784         loggingItemAnimator.reset();
785         loggingItemAnimator.expectRunPendingAnimationsCall(1);
786         mLayoutManager.expectLayouts(2);
787         mTestAdapter.deleteAndNotify(removePos, 1);
788         mLayoutManager.waitForLayout(1);
789         loggingItemAnimator.waitForPendingAnimationsCall(2);
790         assertTrue("removed child should receive remove animation",
791                 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
792         for (RecyclerView.ViewHolder vh : toBeMoved) {
793             assertTrue("view holder should be in moved list",
794                     loggingItemAnimator.mMoveVHs.contains(vh));
795         }
796         List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
797         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
798             View child = mLayoutManager.getChildAt(i);
799             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
800             if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
801                 newHolders.add(holder);
802             }
803         }
804         assertTrue("some new children should show up for the new space", newHolders.size() > 0);
805         assertEquals("no items should receive animate add since they are not new", 0,
806                 loggingItemAnimator.mAddVHs.size());
807         for (RecyclerView.ViewHolder holder : newHolders) {
808             assertTrue("new holder should receive a move animation",
809                     loggingItemAnimator.mMoveVHs.contains(holder));
810         }
811         assertTrue("control against adding too many children due to bad layout state preparation."
812                         + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
813                 mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/);
814     }
815 
waitOneCycle()816     void waitOneCycle() throws Throwable {
817         mActivityRule.runOnUiThread(new Runnable() {
818             @Override
819             public void run() {
820             }
821         });
822     }
823 
824     @Test
hiddenNoneRemoveViewAccessibility()825     public void hiddenNoneRemoveViewAccessibility() throws Throwable {
826         // TODO(b/263592347): remove the RecyclerView.setDebugAssertionsEnabled calls
827         //  and combine this into the impl method
828         // This is just a separate method to temporarily wrap the whole thing in a try/finally
829         // block without messing with git history too much.
830         RecyclerView.setDebugAssertionsEnabled(false);
831         try {
832             hiddenNoneRemoveViewAccessibilityImpl();
833         } finally {
834             RecyclerView.setDebugAssertionsEnabled(true);
835         }
836     }
837 
hiddenNoneRemoveViewAccessibilityImpl()838     public void hiddenNoneRemoveViewAccessibilityImpl() throws Throwable {
839         final Config config = new Config();
840         int adapterSize = 1000;
841         final boolean[] firstItemSpecialSize = new boolean[] {false};
842         TestAdapter adapter = new TestAdapter(adapterSize) {
843             @Override
844             public void onBindViewHolder(@NonNull TestViewHolder holder,
845                     int position) {
846                 super.onBindViewHolder(holder, position);
847                 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
848                 if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
849                     lp = new ViewGroup.MarginLayoutParams(0, 0);
850                     holder.itemView.setLayoutParams(lp);
851                 }
852                 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
853                 final int maxSize;
854                 if (config.mOrientation == HORIZONTAL) {
855                     maxSize = mRecyclerView.getWidth();
856                     mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
857                 } else {
858                     maxSize = mRecyclerView.getHeight();
859                     mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
860                 }
861 
862                 final int desiredSize;
863                 if (position == 0 && firstItemSpecialSize[0]) {
864                     desiredSize = maxSize / 3;
865                 } else {
866                     desiredSize = maxSize / 8;
867                 }
868                 if (config.mOrientation == HORIZONTAL) {
869                     mlp.width = desiredSize;
870                 } else {
871                     mlp.height = desiredSize;
872                 }
873             }
874 
875             @Override
876             public void onBindViewHolder(TestViewHolder holder,
877                     int position, List<Object> payloads) {
878                 onBindViewHolder(holder, position);
879             }
880         };
881         adapter.setHasStableIds(false);
882         config.adapter(adapter);
883         setupByConfig(config, true);
884         // Using ItemAnimatorDouble so we must end all animations manually at the end of the test.
885         final ItemAnimatorTestDouble itemAnimator = new ItemAnimatorTestDouble();
886         mRecyclerView.setItemAnimator(itemAnimator);
887 
888         // push last item out by increasing first item's size
889         final int childBeingPushOut = mLayoutManager.getChildCount() - 1;
890         RecyclerView.ViewHolder itemViewHolder = mRecyclerView
891                 .findViewHolderForAdapterPosition(childBeingPushOut);
892         final int originalAccessibility = ViewCompat.getImportantForAccessibility(
893                 itemViewHolder.itemView);
894         assertTrue(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO == originalAccessibility
895                 || View.IMPORTANT_FOR_ACCESSIBILITY_YES == originalAccessibility);
896 
897         itemAnimator.expect(ItemAnimatorTestDouble.MOVE_START, 1);
898         mActivityRule.runOnUiThread(new Runnable() {
899             @Override
900             public void run() {
901                 firstItemSpecialSize[0] = true;
902                 mTestAdapter.notifyItemChanged(0, "XXX");
903             }
904         });
905         // wait till itemAnimator starts which will block itemView's accessibility
906         itemAnimator.waitFor(ItemAnimatorTestDouble.MOVE_START);
907         // RV Changes accessiblity after onMoveStart, so wait one more cycle.
908         waitOneCycle();
909         assertTrue(itemAnimator.getMovesAnimations().contains(itemViewHolder));
910         assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
911                 itemViewHolder.itemView.getImportantForAccessibility());
912 
913         // notify Change again to run predictive animation.
914         mLayoutManager.expectLayouts(2);
915         mActivityRule.runOnUiThread(new Runnable() {
916             @Override
917             public void run() {
918                 mTestAdapter.notifyItemChanged(0, "XXX");
919             }
920         });
921         mLayoutManager.waitForLayout(1);
922         mActivityRule.runOnUiThread(new Runnable() {
923             @Override
924             public void run() {
925                 itemAnimator.endAnimations();
926             }
927         });
928         // scroll to the view being pushed out, it should get same view from cache as the item
929         // in adapter does not change.
930         smoothScrollToPosition(childBeingPushOut);
931         RecyclerView.ViewHolder itemViewHolder2 = mRecyclerView
932                 .findViewHolderForAdapterPosition(childBeingPushOut);
933         assertSame(itemViewHolder, itemViewHolder2);
934         // the important for accessibility should be reset to YES/AUTO:
935         final int newAccessibility = ViewCompat.getImportantForAccessibility(
936                 itemViewHolder.itemView);
937         assertTrue(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO == newAccessibility
938                 || View.IMPORTANT_FOR_ACCESSIBILITY_YES == newAccessibility);
939     }
940 
941     @Test
layoutSuppressedBug70402422()942     public void layoutSuppressedBug70402422() throws Throwable {
943         final Config config = new Config();
944         TestAdapter adapter = new TestAdapter(2);
945         adapter.setHasStableIds(false);
946         config.adapter(adapter);
947         setupByConfig(config, true);
948         // Using ItemAnimatorDouble so we must end all animations manually at the end of the test.
949         final ItemAnimatorTestDouble itemAnimator = new ItemAnimatorTestDouble();
950         mRecyclerView.setItemAnimator(itemAnimator);
951 
952         final View firstItemView = mRecyclerView
953                 .findViewHolderForAdapterPosition(0).itemView;
954 
955         itemAnimator.expect(ItemAnimatorTestDouble.REMOVE_START, 1);
956         mTestAdapter.deleteAndNotify(1, 1);
957         itemAnimator.waitFor(ItemAnimatorTestDouble.REMOVE_START);
958 
959         mActivityRule.runOnUiThread(new Runnable() {
960             @Override
961             public void run() {
962                 mRecyclerView.suppressLayout(true);
963             }
964         });
965         // requestLayout during item animation, which should be eaten by suppressLayout(true)
966         mActivityRule.runOnUiThread(new Runnable() {
967             @Override
968             public void run() {
969                 firstItemView.requestLayout();
970             }
971         });
972         assertTrue(firstItemView.isLayoutRequested());
973         assertFalse(mRecyclerView.isLayoutRequested());
974         mActivityRule.runOnUiThread(new Runnable() {
975             @Override
976             public void run() {
977                 itemAnimator.endAnimations();
978             }
979         });
980         // When suppressLayout(false), the firstItemView should run a layout pass and clear
981         // isLayoutRequested() flag.
982         mLayoutManager.expectLayouts(1);
983         mActivityRule.runOnUiThread(new Runnable() {
984             @Override
985             public void run() {
986                 mRecyclerView.suppressLayout(false);
987             }
988         });
989         mLayoutManager.waitForLayout(1);
990         assertFalse(firstItemView.isLayoutRequested());
991         assertFalse(mRecyclerView.isLayoutRequested());
992     }
993 
994     @Test
keepFocusOnRelayout()995     public void keepFocusOnRelayout() throws Throwable {
996         setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true);
997         int center = (mLayoutManager.findLastVisibleItemPosition()
998                 - mLayoutManager.findFirstVisibleItemPosition()) / 2;
999         final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center);
1000         final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView);
1001         requestFocus(vh.itemView, true);
1002         assertTrue("view should have the focus", vh.itemView.hasFocus());
1003         // add a bunch of items right before that view, make sure it keeps its position
1004         mLayoutManager.expectLayouts(2);
1005         final int childCountToAdd = mRecyclerView.getChildCount() * 2;
1006         mTestAdapter.addAndNotify(center, childCountToAdd);
1007         center += childCountToAdd; // offset item
1008         mLayoutManager.waitForLayout(2);
1009         mLayoutManager.waitForAnimationsToEnd(20);
1010         final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center);
1011         assertNotNull("focused child should stay in layout", postVH);
1012         assertSame("same view holder should be kept for unchanged child", vh, postVH);
1013         assertEquals("focused child's screen position should stay unchanged", top,
1014                 mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView));
1015     }
1016 
1017     @Test
keepFullFocusOnResize()1018     public void keepFullFocusOnResize() throws Throwable {
1019         keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true);
1020     }
1021 
1022     @Test
keepPartialFocusOnResize()1023     public void keepPartialFocusOnResize() throws Throwable {
1024         keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false);
1025     }
1026 
1027     @Test
keepReverseFullFocusOnResize()1028     public void keepReverseFullFocusOnResize() throws Throwable {
1029         keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true);
1030     }
1031 
1032     @Test
keepReversePartialFocusOnResize()1033     public void keepReversePartialFocusOnResize() throws Throwable {
1034         keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false);
1035     }
1036 
1037     @Test
keepStackFromEndFullFocusOnResize()1038     public void keepStackFromEndFullFocusOnResize() throws Throwable {
1039         keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true);
1040     }
1041 
1042     @Test
keepStackFromEndPartialFocusOnResize()1043     public void keepStackFromEndPartialFocusOnResize() throws Throwable {
1044         keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false);
1045     }
1046 
keepFocusOnResizeTest(final Config config, boolean fullyVisible)1047     public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable {
1048         setupByConfig(config, true);
1049         final int targetPosition;
1050         if (config.mStackFromEnd) {
1051             targetPosition = mLayoutManager.findFirstVisibleItemPosition();
1052         } else {
1053             targetPosition = mLayoutManager.findLastVisibleItemPosition();
1054         }
1055         final OrientationHelper helper = mLayoutManager.mOrientationHelper;
1056         final RecyclerView.ViewHolder vh = mRecyclerView
1057                 .findViewHolderForLayoutPosition(targetPosition);
1058 
1059         // scroll enough to offset the child
1060         int startMargin = helper.getDecoratedStart(vh.itemView) -
1061                 helper.getStartAfterPadding();
1062         int endMargin = helper.getEndAfterPadding() -
1063                 helper.getDecoratedEnd(vh.itemView);
1064         Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin);
1065         requestFocus(vh.itemView, true);
1066         assertTrue("view should gain the focus", vh.itemView.hasFocus());
1067         // scroll enough to offset the child
1068         startMargin = helper.getDecoratedStart(vh.itemView) -
1069                 helper.getStartAfterPadding();
1070         endMargin = helper.getEndAfterPadding() -
1071                 helper.getDecoratedEnd(vh.itemView);
1072 
1073         Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin);
1074         assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0);
1075 
1076         int expectedOffset = 0;
1077         boolean offsetAtStart = false;
1078         if (!fullyVisible) {
1079             // move it a bit such that it is no more fully visible
1080             final int childSize = helper
1081                     .getDecoratedMeasurement(vh.itemView);
1082             expectedOffset = childSize / 3;
1083             if (startMargin < endMargin) {
1084                 scrollBy(expectedOffset);
1085                 offsetAtStart = true;
1086             } else {
1087                 scrollBy(-expectedOffset);
1088                 offsetAtStart = false;
1089             }
1090             startMargin = helper.getDecoratedStart(vh.itemView) -
1091                     helper.getStartAfterPadding();
1092             endMargin = helper.getEndAfterPadding() -
1093                     helper.getDecoratedEnd(vh.itemView);
1094             assertTrue("Assumption check, view should not be fully visible", startMargin < 0
1095                     || endMargin < 0);
1096         }
1097 
1098         mLayoutManager.expectLayouts(1);
1099         mActivityRule.runOnUiThread(new Runnable() {
1100             @Override
1101             public void run() {
1102                 final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams();
1103                 if (config.mOrientation == HORIZONTAL) {
1104                     layoutParams.width = mRecyclerView.getWidth() / 2;
1105                 } else {
1106                     layoutParams.height = mRecyclerView.getHeight() / 2;
1107                 }
1108                 mRecyclerView.setLayoutParams(layoutParams);
1109             }
1110         });
1111         Thread.sleep(100);
1112         // add a bunch of items right before that view, make sure it keeps its position
1113         mLayoutManager.waitForLayout(2);
1114         mLayoutManager.waitForAnimationsToEnd(20);
1115         assertTrue("view should preserve the focus", vh.itemView.hasFocus());
1116         final RecyclerView.ViewHolder postVH = mRecyclerView
1117                 .findViewHolderForLayoutPosition(targetPosition);
1118         assertNotNull("focused child should stay in layout", postVH);
1119         assertSame("same view holder should be kept for unchanged child", vh, postVH);
1120         View focused = postVH.itemView;
1121 
1122         startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding();
1123         endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused);
1124 
1125         assertTrue("focused child should be somewhat visible",
1126                 helper.getDecoratedStart(focused) < helper.getEndAfterPadding()
1127                         && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding());
1128         if (fullyVisible) {
1129             assertTrue("focused child end should stay fully visible",
1130                     endMargin >= 0);
1131             assertTrue("focused child start should stay fully visible",
1132                     startMargin >= 0);
1133         } else {
1134             if (offsetAtStart) {
1135                 assertTrue("start should preserve its offset", startMargin < 0);
1136                 assertTrue("end should be visible", endMargin >= 0);
1137             } else {
1138                 assertTrue("end should preserve its offset", endMargin < 0);
1139                 assertTrue("start should be visible", startMargin >= 0);
1140             }
1141         }
1142     }
1143 
1144     @Test
scrollToPositionWithPredictive()1145     public void scrollToPositionWithPredictive() throws Throwable {
1146         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
1147         removeRecyclerView();
1148         scrollToPositionWithPredictive(3, 20);
1149         removeRecyclerView();
1150         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
1151                 LinearLayoutManager.INVALID_OFFSET);
1152         removeRecyclerView();
1153         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
1154     }
1155 
1156     @Test
recycleDuringAnimations()1157     public void recycleDuringAnimations() throws Throwable {
1158         final AtomicInteger childCount = new AtomicInteger(0);
1159         final TestAdapter adapter = new TestAdapter(300) {
1160             @Override
1161             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
1162                     int viewType) {
1163                 final int cnt = childCount.incrementAndGet();
1164                 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
1165                 if (DEBUG) {
1166                     Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder);
1167                 }
1168                 return testViewHolder;
1169             }
1170         };
1171         setupByConfig(new Config(VERTICAL, false, false).itemCount(300)
1172                 .adapter(adapter), true);
1173 
1174         final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
1175             @Override
1176             public void putRecycledView(RecyclerView.ViewHolder scrap) {
1177                 super.putRecycledView(scrap);
1178                 int cnt = childCount.decrementAndGet();
1179                 if (DEBUG) {
1180                     Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap);
1181                 }
1182             }
1183 
1184             @Override
1185             public RecyclerView.ViewHolder getRecycledView(int viewType) {
1186                 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
1187                 if (recycledView != null) {
1188                     final int cnt = childCount.incrementAndGet();
1189                     if (DEBUG) {
1190                         Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView);
1191                     }
1192                 }
1193                 return recycledView;
1194             }
1195         };
1196         pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500);
1197         mRecyclerView.setRecycledViewPool(pool);
1198 
1199 
1200         // now keep adding children to trigger more children being created etc.
1201         for (int i = 0; i < 100; i ++) {
1202             adapter.addAndNotify(15, 1);
1203             Thread.sleep(15);
1204         }
1205         getInstrumentation().waitForIdleSync();
1206         waitForAnimations(2);
1207         assertEquals("Children count should add up", childCount.get(),
1208                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
1209 
1210         // now trigger lots of add again, followed by a scroll to position
1211         for (int i = 0; i < 100; i ++) {
1212             adapter.addAndNotify(5 + (i % 3) * 3, 1);
1213             Thread.sleep(25);
1214         }
1215 
1216         final AtomicInteger lastVisiblePosition = new AtomicInteger();
1217         mActivityRule.runOnUiThread(new Runnable() {
1218             @Override
1219             public void run() {
1220                 lastVisiblePosition.set(mLayoutManager.findLastVisibleItemPosition());
1221             }
1222         });
1223 
1224         smoothScrollToPosition(lastVisiblePosition.get() + 20);
1225         waitForAnimations(2);
1226         getInstrumentation().waitForIdleSync();
1227         assertEquals("Children count should add up", childCount.get(),
1228                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
1229     }
1230 
1231 
1232     @Test
dontRecycleChildrenOnDetach()1233     public void dontRecycleChildrenOnDetach() throws Throwable {
1234         setupByConfig(new Config().recycleChildrenOnDetach(false), true);
1235         mActivityRule.runOnUiThread(new Runnable() {
1236             @Override
1237             public void run() {
1238                 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
1239                 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView);
1240                 assertEquals("No views are recycled", recyclerSize,
1241                         mRecyclerView.mRecycler.getRecycledViewPool().size());
1242             }
1243         });
1244     }
1245 
1246     @Test
recycleChildrenOnDetach()1247     public void recycleChildrenOnDetach() throws Throwable {
1248         setupByConfig(new Config().recycleChildrenOnDetach(true), true);
1249         final int childCount = mLayoutManager.getChildCount();
1250         mActivityRule.runOnUiThread(new Runnable() {
1251             @Override
1252             public void run() {
1253                 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
1254                 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews(
1255                         mTestAdapter.getItemViewType(0), recyclerSize + childCount);
1256                 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView);
1257                 assertEquals("All children should be recycled", childCount + recyclerSize,
1258                         mRecyclerView.mRecycler.getRecycledViewPool().size());
1259             }
1260         });
1261     }
1262 
1263     @Test
scrollAndClear()1264     public void scrollAndClear() throws Throwable {
1265         setupByConfig(new Config(), true);
1266 
1267         assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
1268 
1269         mLayoutManager.expectLayouts(1);
1270         mActivityRule.runOnUiThread(new Runnable() {
1271             @Override
1272             public void run() {
1273                 mLayoutManager.scrollToPositionWithOffset(1, 0);
1274                 mTestAdapter.clearOnUIThread();
1275             }
1276         });
1277         mLayoutManager.waitForLayout(2);
1278 
1279         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
1280     }
1281 
1282     @Test
accessibilityPositions()1283     public void accessibilityPositions() throws Throwable {
1284         setupByConfig(new Config(VERTICAL, false, false), true);
1285         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
1286                 .getCompatAccessibilityDelegate();
1287         final AccessibilityEvent event = AccessibilityEvent.obtain();
1288         mActivityRule.runOnUiThread(new Runnable() {
1289             @Override
1290             public void run() {
1291                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
1292             }
1293         });
1294         assertEquals("result should have first position",
1295                 event.getFromIndex(),
1296                 mLayoutManager.findFirstVisibleItemPosition());
1297         assertEquals("result should have last position",
1298                 event.getToIndex(),
1299                 mLayoutManager.findLastVisibleItemPosition());
1300     }
1301 
1302     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
1303     @Test
onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()1304     public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
1305             throws Throwable {
1306         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
1307         final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
1308 
1309         assertFalse(nodeInfo.getActionList().contains(
1310                 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
1311         mActivityRule.runOnUiThread(new Runnable() {
1312             @Override
1313             public void run() {
1314                 mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo);
1315             }
1316         });
1317 
1318         assertFalse(nodeInfo.getActionList().contains(
1319                 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
1320     }
1321 
1322     @SdkSuppress(minSdkVersion = 23) // b/271602453
1323     @Test
onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()1324     public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()
1325             throws Throwable {
1326         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(1)), false);
1327         final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
1328 
1329         assertFalse(nodeInfo.getActionList().contains(
1330                 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
1331         mActivityRule.runOnUiThread(new Runnable() {
1332             @Override
1333             public void run() {
1334                 mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo);
1335             }
1336         });
1337 
1338         assertTrue(nodeInfo.getActionList().contains(
1339                 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
1340     }
1341 
1342     @Test
performAccessibilityAction_actionScrollToPosition_withTooLowPosition()1343     public void performAccessibilityAction_actionScrollToPosition_withTooLowPosition()
1344             throws Throwable {
1345         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
1346         assertFirstItemIsAtTop();
1347 
1348         final boolean[] returnValue = {false};
1349         Bundle arguments = new Bundle();
1350         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
1351         mActivityRule.runOnUiThread(new Runnable() {
1352             @Override
1353             public void run() {
1354                 returnValue[0] = mLayoutManager.performAccessibilityAction(
1355                         android.R.id.accessibilityActionScrollToPosition, arguments);
1356             }
1357         });
1358         mLayoutManager.waitForLayout(2);
1359 
1360         assertFalse(returnValue[0]);
1361         assertFirstItemIsAtTop();
1362     }
1363 
1364     @Test
performAccessibilityAction_actionScrollToPosition_verticalWithNoRowArg()1365     public void performAccessibilityAction_actionScrollToPosition_verticalWithNoRowArg()
1366             throws Throwable {
1367         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
1368         assertFirstItemIsAtTop();
1369 
1370         final boolean[] returnValue = {false};
1371         Bundle arguments = new Bundle();
1372         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 30);
1373         mActivityRule.runOnUiThread(new Runnable() {
1374             @Override
1375             public void run() {
1376                 returnValue[0] = mLayoutManager.performAccessibilityAction(
1377                         android.R.id.accessibilityActionScrollToPosition, arguments);
1378             }
1379         });
1380         mLayoutManager.waitForLayout(2);
1381 
1382         assertFalse(returnValue[0]);
1383         assertFirstItemIsAtTop();
1384     }
1385 
1386     @Test
performAccessibilityAction_actionScrollToPosition_horizontalWithNoColumnArg()1387     public void performAccessibilityAction_actionScrollToPosition_horizontalWithNoColumnArg()
1388             throws Throwable {
1389         setupByConfig(new Config(HORIZONTAL, false, false).adapter(new TestAdapter(30)), true);
1390         assertFirstItemIsAtTop();
1391 
1392         final boolean[] returnValue = {false};
1393         Bundle arguments = new Bundle();
1394         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
1395         mActivityRule.runOnUiThread(new Runnable() {
1396             @Override
1397             public void run() {
1398                 returnValue[0] = mLayoutManager.performAccessibilityAction(
1399                         android.R.id.accessibilityActionScrollToPosition, arguments);
1400             }
1401         });
1402         mLayoutManager.waitForLayout(2);
1403 
1404         assertFalse(returnValue[0]);
1405         assertFirstItemIsAtTop();
1406     }
1407 
1408     @Test
performAccessibilityAction_actionScrollToPosition_verticalWithRowArg_scrolls()1409     public void performAccessibilityAction_actionScrollToPosition_verticalWithRowArg_scrolls()
1410             throws Throwable {
1411         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
1412         assertFirstItemIsAtTop();
1413 
1414         final boolean[] returnValue = {false};
1415         Bundle arguments = new Bundle();
1416         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
1417         // The column argument is ignored in VERTICAL orientation.
1418         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 30);
1419         mActivityRule.runOnUiThread(new Runnable() {
1420             @Override
1421             public void run() {
1422                 returnValue[0] = mLayoutManager.performAccessibilityAction(
1423                         android.R.id.accessibilityActionScrollToPosition, arguments);
1424             }
1425         });
1426         mLayoutManager.waitForLayout(2);
1427 
1428         assertTrue(returnValue[0]);
1429         assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (11)");
1430     }
1431 
1432     @Test
performAccessibilityAction_actionScrollToPosition_horizontalWithColumnArg_scrolls()1433     public void performAccessibilityAction_actionScrollToPosition_horizontalWithColumnArg_scrolls()
1434             throws Throwable {
1435         setupByConfig(new Config(HORIZONTAL, false, false).adapter(new TestAdapter(30)), true);
1436         assertFirstItemIsAtTop();
1437 
1438         final boolean[] returnValue = {false};
1439         Bundle arguments = new Bundle();
1440         // The row argument is ignored in HORIZONTAL orientation.
1441         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 30);
1442         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 10);
1443         mActivityRule.runOnUiThread(new Runnable() {
1444             @Override
1445             public void run() {
1446                 returnValue[0] = mLayoutManager.performAccessibilityAction(
1447                         android.R.id.accessibilityActionScrollToPosition, arguments);
1448             }
1449         });
1450         mLayoutManager.waitForLayout(2);
1451 
1452         assertTrue(returnValue[0]);
1453         assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (11)");
1454     }
1455 
1456 
1457     @Test
performAccessibilityAction_actionScrollToPosition_withTooHighPosition_scrollsToEnd()1458     public void performAccessibilityAction_actionScrollToPosition_withTooHighPosition_scrollsToEnd()
1459             throws Throwable {
1460         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
1461         assertFirstItemIsAtTop();
1462 
1463         final boolean[] returnValue = {false};
1464         Bundle arguments = new Bundle();
1465         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 1000);
1466         mActivityRule.runOnUiThread(new Runnable() {
1467             @Override
1468             public void run() {
1469                 returnValue[0] = mLayoutManager.performAccessibilityAction(
1470                         android.R.id.accessibilityActionScrollToPosition, arguments);
1471             }
1472         });
1473         mLayoutManager.waitForLayout(2);
1474 
1475         assertTrue(returnValue[0]);
1476         assertEquals(((TextView) mLayoutManager.getChildAt(
1477                 mLayoutManager.getChildCount() - 1)).getText(), "Item (30)");
1478     }
1479 
assertFirstItemIsAtTop()1480     private void assertFirstItemIsAtTop() {
1481         assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (1)");
1482     }
1483 
1484     @Test
onInitializeAccessibilityNodeInfo_noAdapter()1485     public void onInitializeAccessibilityNodeInfo_noAdapter() throws Throwable {
1486         mRecyclerView = inflateWrappedRV();
1487         mLayoutManager = new WrappedLinearLayoutManager(
1488                 getActivity(), LinearLayoutManager.VERTICAL, false);
1489         mRecyclerView.setLayoutManager(mLayoutManager);
1490 
1491         AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
1492         mActivityRule.runOnUiThread(() -> {
1493             mLayoutManager.onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler,
1494                     mRecyclerView.mState, nodeInfo);
1495         });
1496 
1497         assertThat(nodeInfo.getActionList()).doesNotContain(
1498                 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
1499 
1500     }
1501 }
1502