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