Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.support.v7.widget;
     18 
     19 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
     20 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
     21 
     22 import static org.hamcrest.CoreMatchers.is;
     23 import static org.junit.Assert.assertEquals;
     24 import static org.junit.Assert.assertFalse;
     25 import static org.junit.Assert.assertNotNull;
     26 import static org.junit.Assert.assertNull;
     27 import static org.junit.Assert.assertSame;
     28 import static org.junit.Assert.assertThat;
     29 import static org.junit.Assert.assertTrue;
     30 
     31 import android.graphics.Color;
     32 import android.graphics.drawable.ColorDrawable;
     33 import android.graphics.drawable.StateListDrawable;
     34 import android.os.Build;
     35 import android.support.test.filters.LargeTest;
     36 import android.support.test.filters.SdkSuppress;
     37 import android.support.v4.view.AccessibilityDelegateCompat;
     38 import android.util.Log;
     39 import android.util.StateSet;
     40 import android.view.View;
     41 import android.view.ViewGroup;
     42 import android.view.accessibility.AccessibilityEvent;
     43 
     44 import org.junit.Test;
     45 
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 import java.util.concurrent.CountDownLatch;
     49 import java.util.concurrent.TimeUnit;
     50 import java.util.concurrent.atomic.AtomicInteger;
     51 
     52 
     53 /**
     54  * Includes tests for {@link LinearLayoutManager}.
     55  * <p>
     56  * Since most UI tests are not practical, these tests are focused on internal data representation
     57  * and stability of LinearLayoutManager in response to different events (state change, scrolling
     58  * etc) where it is very hard to do manual testing.
     59  */
     60 @LargeTest
     61 public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest {
     62 
     63     @Test
     64     public void topUnfocusableViewsVisibility() throws Throwable {
     65         // The maximum number of child views that can be visible at any time.
     66         final int visibleChildCount = 5;
     67         final int consecutiveFocusablesCount = 2;
     68         final int consecutiveUnFocusablesCount = 18;
     69         final TestAdapter adapter = new TestAdapter(
     70                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
     71             RecyclerView mAttachedRv;
     72 
     73             @Override
     74             public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
     75                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
     76                 // Good to have colors for debugging
     77                 StateListDrawable stl = new StateListDrawable();
     78                 stl.addState(new int[]{android.R.attr.state_focused},
     79                         new ColorDrawable(Color.RED));
     80                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
     81                 //noinspection deprecation used to support kitkat tests
     82                 testViewHolder.itemView.setBackgroundDrawable(stl);
     83                 return testViewHolder;
     84             }
     85 
     86             @Override
     87             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
     88                 mAttachedRv = recyclerView;
     89             }
     90 
     91             @Override
     92             public void onBindViewHolder(TestViewHolder holder,
     93                     int position) {
     94                 super.onBindViewHolder(holder, position);
     95                 if (position < consecutiveFocusablesCount) {
     96                     holder.itemView.setFocusable(true);
     97                     holder.itemView.setFocusableInTouchMode(true);
     98                 } else {
     99                     holder.itemView.setFocusable(false);
    100                     holder.itemView.setFocusableInTouchMode(false);
    101                 }
    102                 // This height ensures that some portion of #visibleChildCount'th child is
    103                 // off-bounds, creating more interesting test scenario.
    104                 holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
    105                         + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
    106             }
    107         };
    108         setupByConfig(new Config(VERTICAL, false, false).adapter(adapter).reverseLayout(true),
    109                 false);
    110         waitForFirstLayout();
    111 
    112         // adapter position of the currently focused item.
    113         int focusIndex = 0;
    114         View newFocused = mRecyclerView.getChildAt(focusIndex);
    115         requestFocus(newFocused, true);
    116         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
    117                 focusIndex);
    118         assertThat("Child at position " + focusIndex + " should be focused",
    119                 toFocus.itemView.hasFocus(), is(true));
    120 
    121         // adapter position of the item (whether focusable or not) that just becomes fully
    122         // visible after focusSearch.
    123         int visibleIndex = 0;
    124         // The VH of the above adapter position
    125         RecyclerView.ViewHolder toVisible = null;
    126 
    127         // Navigate up through the focusable and unfocusable chunks. The focusable items should
    128         // become focused one by one until hitting the last focusable item, at which point,
    129         // unfocusable items should become visible on the screen until the currently focused item
    130         // stays on the screen.
    131         for (int i = 0; i < adapter.getItemCount(); i++) {
    132             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_UP, true);
    133             // adapter position of the currently focused item.
    134             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
    135             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
    136             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
    137                     (visibleIndex + 1));
    138             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
    139 
    140             assertThat("Child at position " + focusIndex + " should be focused",
    141                     toFocus.itemView.hasFocus(), is(true));
    142             assertTrue("Focused child should be at least partially visible.",
    143                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
    144             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
    145                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
    146         }
    147     }
    148 
    149     @Test
    150     public void bottomUnfocusableViewsVisibility() throws Throwable {
    151         // The maximum number of child views that can be visible at any time.
    152         final int visibleChildCount = 5;
    153         final int consecutiveFocusablesCount = 2;
    154         final int consecutiveUnFocusablesCount = 18;
    155         final TestAdapter adapter = new TestAdapter(
    156                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
    157             RecyclerView mAttachedRv;
    158 
    159             @Override
    160             public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    161                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
    162                 // Good to have colors for debugging
    163                 StateListDrawable stl = new StateListDrawable();
    164                 stl.addState(new int[]{android.R.attr.state_focused},
    165                         new ColorDrawable(Color.RED));
    166                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
    167                 //noinspection deprecation used to support kitkat tests
    168                 testViewHolder.itemView.setBackgroundDrawable(stl);
    169                 return testViewHolder;
    170             }
    171 
    172             @Override
    173             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    174                 mAttachedRv = recyclerView;
    175             }
    176 
    177             @Override
    178             public void onBindViewHolder(TestViewHolder holder,
    179                     int position) {
    180                 super.onBindViewHolder(holder, position);
    181                 if (position < consecutiveFocusablesCount) {
    182                     holder.itemView.setFocusable(true);
    183                     holder.itemView.setFocusableInTouchMode(true);
    184                 } else {
    185                     holder.itemView.setFocusable(false);
    186                     holder.itemView.setFocusableInTouchMode(false);
    187                 }
    188                 // This height ensures that some portion of #visibleChildCount'th child is
    189                 // off-bounds, creating more interesting test scenario.
    190                 holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
    191                         + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
    192             }
    193         };
    194         setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
    195         waitForFirstLayout();
    196 
    197         // adapter position of the currently focused item.
    198         int focusIndex = 0;
    199         View newFocused = mRecyclerView.getChildAt(focusIndex);
    200         requestFocus(newFocused, true);
    201         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
    202                 focusIndex);
    203         assertThat("Child at position " + focusIndex + " should be focused",
    204                 toFocus.itemView.hasFocus(), is(true));
    205 
    206         // adapter position of the item (whether focusable or not) that just becomes fully
    207         // visible after focusSearch.
    208         int visibleIndex = 0;
    209         // The VH of the above adapter position
    210         RecyclerView.ViewHolder toVisible = null;
    211 
    212         // Navigate down through the focusable and unfocusable chunks. The focusable items should
    213         // become focused one by one until hitting the last focusable item, at which point,
    214         // unfocusable items should become visible on the screen until the currently focused item
    215         // stays on the screen.
    216         for (int i = 0; i < adapter.getItemCount(); i++) {
    217             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
    218             // adapter position of the currently focused item.
    219             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
    220             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
    221             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
    222                     (visibleIndex + 1));
    223             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
    224 
    225             assertThat("Child at position " + focusIndex + " should be focused",
    226                     toFocus.itemView.hasFocus(), is(true));
    227             assertTrue("Focused child should be at least partially visible.",
    228                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
    229             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
    230                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
    231         }
    232     }
    233 
    234     @Test
    235     public void leftUnfocusableViewsVisibility() throws Throwable {
    236         // The maximum number of child views that can be visible at any time.
    237         final int visibleChildCount = 5;
    238         final int consecutiveFocusablesCount = 2;
    239         final int consecutiveUnFocusablesCount = 18;
    240         final TestAdapter adapter = new TestAdapter(
    241                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
    242             RecyclerView mAttachedRv;
    243 
    244             @Override
    245             public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    246                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
    247                 // Good to have colors for debugging
    248                 StateListDrawable stl = new StateListDrawable();
    249                 stl.addState(new int[]{android.R.attr.state_focused},
    250                         new ColorDrawable(Color.RED));
    251                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
    252                 //noinspection deprecation used to support kitkat tests
    253                 testViewHolder.itemView.setBackgroundDrawable(stl);
    254                 return testViewHolder;
    255             }
    256 
    257             @Override
    258             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    259                 mAttachedRv = recyclerView;
    260             }
    261 
    262             @Override
    263             public void onBindViewHolder(TestViewHolder holder,
    264                     int position) {
    265                 super.onBindViewHolder(holder, position);
    266                 if (position < consecutiveFocusablesCount) {
    267                     holder.itemView.setFocusable(true);
    268                     holder.itemView.setFocusableInTouchMode(true);
    269                 } else {
    270                     holder.itemView.setFocusable(false);
    271                     holder.itemView.setFocusableInTouchMode(false);
    272                 }
    273                 // This width ensures that some portion of #visibleChildCount'th child is
    274                 // off-bounds, creating more interesting test scenario.
    275                 holder.itemView.setMinimumWidth((mAttachedRv.getWidth()
    276                         + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount);
    277             }
    278         };
    279         setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter).reverseLayout(true),
    280                 false);
    281         waitForFirstLayout();
    282 
    283         // adapter position of the currently focused item.
    284         int focusIndex = 0;
    285         View newFocused = mRecyclerView.getChildAt(focusIndex);
    286         requestFocus(newFocused, true);
    287         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
    288                 focusIndex);
    289         assertThat("Child at position " + focusIndex + " should be focused",
    290                 toFocus.itemView.hasFocus(), is(true));
    291 
    292         // adapter position of the item (whether focusable or not) that just becomes fully
    293         // visible after focusSearch.
    294         int visibleIndex = 0;
    295         // The VH of the above adapter position
    296         RecyclerView.ViewHolder toVisible = null;
    297 
    298         // Navigate left through the focusable and unfocusable chunks. The focusable items should
    299         // become focused one by one until hitting the last focusable item, at which point,
    300         // unfocusable items should become visible on the screen until the currently focused item
    301         // stays on the screen.
    302         for (int i = 0; i < adapter.getItemCount(); i++) {
    303             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
    304             // adapter position of the currently focused item.
    305             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
    306             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
    307             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
    308                     (visibleIndex + 1));
    309             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
    310 
    311             assertThat("Child at position " + focusIndex + " should be focused",
    312                     toFocus.itemView.hasFocus(), is(true));
    313             assertTrue("Focused child should be at least partially visible.",
    314                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
    315             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
    316                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
    317         }
    318     }
    319 
    320     @Test
    321     public void rightUnfocusableViewsVisibility() throws Throwable {
    322         // The maximum number of child views that can be visible at any time.
    323         final int visibleChildCount = 5;
    324         final int consecutiveFocusablesCount = 2;
    325         final int consecutiveUnFocusablesCount = 18;
    326         final TestAdapter adapter = new TestAdapter(
    327                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
    328             RecyclerView mAttachedRv;
    329 
    330             @Override
    331             public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    332                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
    333                 // Good to have colors for debugging
    334                 StateListDrawable stl = new StateListDrawable();
    335                 stl.addState(new int[]{android.R.attr.state_focused},
    336                         new ColorDrawable(Color.RED));
    337                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
    338                 //noinspection deprecation used to support kitkat tests
    339                 testViewHolder.itemView.setBackgroundDrawable(stl);
    340                 return testViewHolder;
    341             }
    342 
    343             @Override
    344             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    345                 mAttachedRv = recyclerView;
    346             }
    347 
    348             @Override
    349             public void onBindViewHolder(TestViewHolder holder,
    350                     int position) {
    351                 super.onBindViewHolder(holder, position);
    352                 if (position < consecutiveFocusablesCount) {
    353                     holder.itemView.setFocusable(true);
    354                     holder.itemView.setFocusableInTouchMode(true);
    355                 } else {
    356                     holder.itemView.setFocusable(false);
    357                     holder.itemView.setFocusableInTouchMode(false);
    358                 }
    359                 // This width ensures that some portion of #visibleChildCount'th child is
    360                 // off-bounds, creating more interesting test scenario.
    361                 holder.itemView.setMinimumWidth((mAttachedRv.getWidth()
    362                         + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount);
    363             }
    364         };
    365         setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter), false);
    366         waitForFirstLayout();
    367 
    368         // adapter position of the currently focused item.
    369         int focusIndex = 0;
    370         View newFocused = mRecyclerView.getChildAt(focusIndex);
    371         requestFocus(newFocused, true);
    372         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
    373                 focusIndex);
    374         assertThat("Child at position " + focusIndex + " should be focused",
    375                 toFocus.itemView.hasFocus(), is(true));
    376 
    377         // adapter position of the item (whether focusable or not) that just becomes fully
    378         // visible after focusSearch.
    379         int visibleIndex = 0;
    380         // The VH of the above adapter position
    381         RecyclerView.ViewHolder toVisible = null;
    382 
    383         // Navigate right through the focusable and unfocusable chunks. The focusable items should
    384         // become focused one by one until hitting the last focusable item, at which point,
    385         // unfocusable items should become visible on the screen until the currently focused item
    386         // stays on the screen.
    387         for (int i = 0; i < adapter.getItemCount(); i++) {
    388             focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
    389             // adapter position of the currently focused item.
    390             focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1));
    391             toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex);
    392             visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2,
    393                     (visibleIndex + 1));
    394             toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex);
    395 
    396             assertThat("Child at position " + focusIndex + " should be focused",
    397                     toFocus.itemView.hasFocus(), is(true));
    398             assertTrue("Focused child should be at least partially visible.",
    399                     isViewPartiallyInBound(mRecyclerView, toFocus.itemView));
    400             assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
    401                     isViewFullyInBound(mRecyclerView, toVisible.itemView));
    402         }
    403     }
    404 
    405     // Run this test on Jelly Bean and newer because clearFocus on API 15 will call
    406     // requestFocus in ViewRootImpl when clearChildFocus is called. Whereas, in API 16 and above,
    407     // this call is delayed until after onFocusChange callback is called. Thus on API 16+, there's a
    408     // transient state of no child having focus during which onFocusChange is executed. This
    409     // transient state does not exist on API 15-.
    410     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
    411     @Test
    412     public void unfocusableScrollingWhenFocusCleared() throws Throwable {
    413         // The maximum number of child views that can be visible at any time.
    414         final int visibleChildCount = 5;
    415         final int consecutiveFocusablesCount = 2;
    416         final int consecutiveUnFocusablesCount = 18;
    417         final TestAdapter adapter = new TestAdapter(
    418                 consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
    419             RecyclerView mAttachedRv;
    420 
    421             @Override
    422             public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    423                 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
    424                 // Good to have colors for debugging
    425                 StateListDrawable stl = new StateListDrawable();
    426                 stl.addState(new int[]{android.R.attr.state_focused},
    427                         new ColorDrawable(Color.RED));
    428                 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
    429                 //noinspection deprecation used to support kitkat tests
    430                 testViewHolder.itemView.setBackgroundDrawable(stl);
    431                 return testViewHolder;
    432             }
    433 
    434             @Override
    435             public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    436                 mAttachedRv = recyclerView;
    437             }
    438 
    439             @Override
    440             public void onBindViewHolder(TestViewHolder holder,
    441                     int position) {
    442                 super.onBindViewHolder(holder, position);
    443                 if (position < consecutiveFocusablesCount) {
    444                     holder.itemView.setFocusable(true);
    445                     holder.itemView.setFocusableInTouchMode(true);
    446                 } else {
    447                     holder.itemView.setFocusable(false);
    448                     holder.itemView.setFocusableInTouchMode(false);
    449                 }
    450                 // This height ensures that some portion of #visibleChildCount'th child is
    451                 // off-bounds, creating more interesting test scenario.
    452                 holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
    453                         + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
    454             }
    455         };
    456         setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
    457         waitForFirstLayout();
    458 
    459         // adapter position of the currently focused item.
    460         int focusIndex = 0;
    461         View newFocused = mRecyclerView.getChildAt(focusIndex);
    462         requestFocus(newFocused, true);
    463         RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
    464                 focusIndex);
    465         assertThat("Child at position " + focusIndex + " should be focused",
    466                 toFocus.itemView.hasFocus(), is(true));
    467 
    468         final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
    469         focusIndex++;
    470         assertThat("Child at position " + focusIndex + " should be focused",
    471                 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
    472                 is(true));
    473         final CountDownLatch focusLatch = new CountDownLatch(1);
    474         mActivityRule.runOnUiThread(new Runnable() {
    475             @Override
    476             public void run() {
    477                 nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){
    478                     @Override
    479                     public void onFocusChange(View v, boolean hasFocus) {
    480                         assertNull("Focus just got cleared and no children should be holding"
    481                                         + " focus now.", mRecyclerView.getFocusedChild());
    482                         try {
    483                             // Calling focusSearch should be a no-op here since even though there
    484                             // are unfocusable views down to scroll to, none of RV's children hold
    485                             // focus at this stage.
    486                             View focusedChild  = focusSearch(v, View.FOCUS_DOWN, true);
    487                             assertNull("Calling focusSearch should be no-op when no children hold"
    488                                     + "focus", focusedChild);
    489                             // No scrolling should have happened, so any unfocusables that were
    490                             // invisible should still be invisible.
    491                             RecyclerView.ViewHolder unforcusablePartiallyVisibleChild =
    492                                     mRecyclerView.findViewHolderForAdapterPosition(
    493                                             visibleChildCount - 1);
    494                             assertFalse("Child view at adapter pos " + (visibleChildCount - 1)
    495                                             + " should not be fully visible.",
    496                                     isViewFullyInBound(mRecyclerView,
    497                                             unforcusablePartiallyVisibleChild.itemView));
    498                         } catch (Throwable t) {
    499                             postExceptionToInstrumentation(t);
    500                         }
    501                     }
    502                 });
    503                 nextView.clearFocus();
    504                 focusLatch.countDown();
    505             }
    506         });
    507         assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
    508         assertThat("Child at position " + focusIndex + " should no longer be focused",
    509                 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
    510                 is(false));
    511     }
    512 
    513     @Test
    514     public void removeAnchorItem() throws Throwable {
    515         removeAnchorItemTest(
    516                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(
    517                         false), 100, 0);
    518     }
    519 
    520     @Test
    521     public void removeAnchorItemReverse() throws Throwable {
    522         removeAnchorItemTest(
    523                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100,
    524                 0);
    525     }
    526 
    527     @Test
    528     public void removeAnchorItemStackFromEnd() throws Throwable {
    529         removeAnchorItemTest(
    530                 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100,
    531                 99);
    532     }
    533 
    534     @Test
    535     public void removeAnchorItemStackFromEndAndReverse() throws Throwable {
    536         removeAnchorItemTest(
    537                 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100,
    538                 99);
    539     }
    540 
    541     @Test
    542     public void removeAnchorItemHorizontal() throws Throwable {
    543         removeAnchorItemTest(
    544                 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(
    545                         false), 100, 0);
    546     }
    547 
    548     @Test
    549     public void removeAnchorItemReverseHorizontal() throws Throwable {
    550         removeAnchorItemTest(
    551                 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true),
    552                 100, 0);
    553     }
    554 
    555     @Test
    556     public void removeAnchorItemStackFromEndHorizontal() throws Throwable {
    557         removeAnchorItemTest(
    558                 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false),
    559                 100, 99);
    560     }
    561 
    562     @Test
    563     public void removeAnchorItemStackFromEndAndReverseHorizontal() throws Throwable {
    564         removeAnchorItemTest(
    565                 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100,
    566                 99);
    567     }
    568 
    569     /**
    570      * This tests a regression where predictive animations were not working as expected when the
    571      * first item is removed and there aren't any more items to add from that direction.
    572      * First item refers to the default anchor item.
    573      */
    574     public void removeAnchorItemTest(final Config config, int adapterSize,
    575             final int removePos) throws Throwable {
    576         config.adapter(new TestAdapter(adapterSize) {
    577             @Override
    578             public void onBindViewHolder(TestViewHolder holder,
    579                     int position) {
    580                 super.onBindViewHolder(holder, position);
    581                 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    582                 if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
    583                     lp = new ViewGroup.MarginLayoutParams(0, 0);
    584                     holder.itemView.setLayoutParams(lp);
    585                 }
    586                 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
    587                 final int maxSize;
    588                 if (config.mOrientation == HORIZONTAL) {
    589                     maxSize = mRecyclerView.getWidth();
    590                     mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
    591                 } else {
    592                     maxSize = mRecyclerView.getHeight();
    593                     mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
    594                 }
    595 
    596                 final int desiredSize;
    597                 if (position == removePos) {
    598                     // make it large
    599                     desiredSize = maxSize / 4;
    600                 } else {
    601                     // make it small
    602                     desiredSize = maxSize / 8;
    603                 }
    604                 if (config.mOrientation == HORIZONTAL) {
    605                     mlp.width = desiredSize;
    606                 } else {
    607                     mlp.height = desiredSize;
    608                 }
    609             }
    610         });
    611         setupByConfig(config, true);
    612         final int childCount = mLayoutManager.getChildCount();
    613         RecyclerView.ViewHolder toBeRemoved = null;
    614         List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
    615         for (int i = 0; i < childCount; i++) {
    616             View child = mLayoutManager.getChildAt(i);
    617             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
    618             if (holder.getAdapterPosition() == removePos) {
    619                 toBeRemoved = holder;
    620             } else {
    621                 toBeMoved.add(holder);
    622             }
    623         }
    624         assertNotNull("test sanity", toBeRemoved);
    625         assertEquals("test sanity", childCount - 1, toBeMoved.size());
    626         LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
    627         mRecyclerView.setItemAnimator(loggingItemAnimator);
    628         loggingItemAnimator.reset();
    629         loggingItemAnimator.expectRunPendingAnimationsCall(1);
    630         mLayoutManager.expectLayouts(2);
    631         mTestAdapter.deleteAndNotify(removePos, 1);
    632         mLayoutManager.waitForLayout(1);
    633         loggingItemAnimator.waitForPendingAnimationsCall(2);
    634         assertTrue("removed child should receive remove animation",
    635                 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
    636         for (RecyclerView.ViewHolder vh : toBeMoved) {
    637             assertTrue("view holder should be in moved list",
    638                     loggingItemAnimator.mMoveVHs.contains(vh));
    639         }
    640         List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
    641         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
    642             View child = mLayoutManager.getChildAt(i);
    643             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
    644             if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
    645                 newHolders.add(holder);
    646             }
    647         }
    648         assertTrue("some new children should show up for the new space", newHolders.size() > 0);
    649         assertEquals("no items should receive animate add since they are not new", 0,
    650                 loggingItemAnimator.mAddVHs.size());
    651         for (RecyclerView.ViewHolder holder : newHolders) {
    652             assertTrue("new holder should receive a move animation",
    653                     loggingItemAnimator.mMoveVHs.contains(holder));
    654         }
    655         assertTrue("control against adding too many children due to bad layout state preparation."
    656                         + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
    657                 mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/);
    658     }
    659 
    660     @Test
    661     public void keepFocusOnRelayout() throws Throwable {
    662         setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true);
    663         int center = (mLayoutManager.findLastVisibleItemPosition()
    664                 - mLayoutManager.findFirstVisibleItemPosition()) / 2;
    665         final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center);
    666         final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView);
    667         requestFocus(vh.itemView, true);
    668         assertTrue("view should have the focus", vh.itemView.hasFocus());
    669         // add a bunch of items right before that view, make sure it keeps its position
    670         mLayoutManager.expectLayouts(2);
    671         final int childCountToAdd = mRecyclerView.getChildCount() * 2;
    672         mTestAdapter.addAndNotify(center, childCountToAdd);
    673         center += childCountToAdd; // offset item
    674         mLayoutManager.waitForLayout(2);
    675         mLayoutManager.waitForAnimationsToEnd(20);
    676         final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center);
    677         assertNotNull("focused child should stay in layout", postVH);
    678         assertSame("same view holder should be kept for unchanged child", vh, postVH);
    679         assertEquals("focused child's screen position should stay unchanged", top,
    680                 mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView));
    681     }
    682 
    683     @Test
    684     public void keepFullFocusOnResize() throws Throwable {
    685         keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true);
    686     }
    687 
    688     @Test
    689     public void keepPartialFocusOnResize() throws Throwable {
    690         keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false);
    691     }
    692 
    693     @Test
    694     public void keepReverseFullFocusOnResize() throws Throwable {
    695         keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true);
    696     }
    697 
    698     @Test
    699     public void keepReversePartialFocusOnResize() throws Throwable {
    700         keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false);
    701     }
    702 
    703     @Test
    704     public void keepStackFromEndFullFocusOnResize() throws Throwable {
    705         keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true);
    706     }
    707 
    708     @Test
    709     public void keepStackFromEndPartialFocusOnResize() throws Throwable {
    710         keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false);
    711     }
    712 
    713     public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable {
    714         setupByConfig(config, true);
    715         final int targetPosition;
    716         if (config.mStackFromEnd) {
    717             targetPosition = mLayoutManager.findFirstVisibleItemPosition();
    718         } else {
    719             targetPosition = mLayoutManager.findLastVisibleItemPosition();
    720         }
    721         final OrientationHelper helper = mLayoutManager.mOrientationHelper;
    722         final RecyclerView.ViewHolder vh = mRecyclerView
    723                 .findViewHolderForLayoutPosition(targetPosition);
    724 
    725         // scroll enough to offset the child
    726         int startMargin = helper.getDecoratedStart(vh.itemView) -
    727                 helper.getStartAfterPadding();
    728         int endMargin = helper.getEndAfterPadding() -
    729                 helper.getDecoratedEnd(vh.itemView);
    730         Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin);
    731         requestFocus(vh.itemView, true);
    732         assertTrue("view should gain the focus", vh.itemView.hasFocus());
    733         // scroll enough to offset the child
    734         startMargin = helper.getDecoratedStart(vh.itemView) -
    735                 helper.getStartAfterPadding();
    736         endMargin = helper.getEndAfterPadding() -
    737                 helper.getDecoratedEnd(vh.itemView);
    738 
    739         Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin);
    740         assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0);
    741 
    742         int expectedOffset = 0;
    743         boolean offsetAtStart = false;
    744         if (!fullyVisible) {
    745             // move it a bit such that it is no more fully visible
    746             final int childSize = helper
    747                     .getDecoratedMeasurement(vh.itemView);
    748             expectedOffset = childSize / 3;
    749             if (startMargin < endMargin) {
    750                 scrollBy(expectedOffset);
    751                 offsetAtStart = true;
    752             } else {
    753                 scrollBy(-expectedOffset);
    754                 offsetAtStart = false;
    755             }
    756             startMargin = helper.getDecoratedStart(vh.itemView) -
    757                     helper.getStartAfterPadding();
    758             endMargin = helper.getEndAfterPadding() -
    759                     helper.getDecoratedEnd(vh.itemView);
    760             assertTrue("test sanity, view should not be fully visible", startMargin < 0
    761                     || endMargin < 0);
    762         }
    763 
    764         mLayoutManager.expectLayouts(1);
    765         mActivityRule.runOnUiThread(new Runnable() {
    766             @Override
    767             public void run() {
    768                 final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams();
    769                 if (config.mOrientation == HORIZONTAL) {
    770                     layoutParams.width = mRecyclerView.getWidth() / 2;
    771                 } else {
    772                     layoutParams.height = mRecyclerView.getHeight() / 2;
    773                 }
    774                 mRecyclerView.setLayoutParams(layoutParams);
    775             }
    776         });
    777         Thread.sleep(100);
    778         // add a bunch of items right before that view, make sure it keeps its position
    779         mLayoutManager.waitForLayout(2);
    780         mLayoutManager.waitForAnimationsToEnd(20);
    781         assertTrue("view should preserve the focus", vh.itemView.hasFocus());
    782         final RecyclerView.ViewHolder postVH = mRecyclerView
    783                 .findViewHolderForLayoutPosition(targetPosition);
    784         assertNotNull("focused child should stay in layout", postVH);
    785         assertSame("same view holder should be kept for unchanged child", vh, postVH);
    786         View focused = postVH.itemView;
    787 
    788         startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding();
    789         endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused);
    790 
    791         assertTrue("focused child should be somewhat visible",
    792                 helper.getDecoratedStart(focused) < helper.getEndAfterPadding()
    793                         && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding());
    794         if (fullyVisible) {
    795             assertTrue("focused child end should stay fully visible",
    796                     endMargin >= 0);
    797             assertTrue("focused child start should stay fully visible",
    798                     startMargin >= 0);
    799         } else {
    800             if (offsetAtStart) {
    801                 assertTrue("start should preserve its offset", startMargin < 0);
    802                 assertTrue("end should be visible", endMargin >= 0);
    803             } else {
    804                 assertTrue("end should preserve its offset", endMargin < 0);
    805                 assertTrue("start should be visible", startMargin >= 0);
    806             }
    807         }
    808     }
    809 
    810     @Test
    811     public void scrollToPositionWithPredictive() throws Throwable {
    812         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
    813         removeRecyclerView();
    814         scrollToPositionWithPredictive(3, 20);
    815         removeRecyclerView();
    816         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
    817                 LinearLayoutManager.INVALID_OFFSET);
    818         removeRecyclerView();
    819         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
    820     }
    821 
    822     @Test
    823     public void recycleDuringAnimations() throws Throwable {
    824         final AtomicInteger childCount = new AtomicInteger(0);
    825         final TestAdapter adapter = new TestAdapter(300) {
    826             @Override
    827             public TestViewHolder onCreateViewHolder(ViewGroup parent,
    828                     int viewType) {
    829                 final int cnt = childCount.incrementAndGet();
    830                 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
    831                 if (DEBUG) {
    832                     Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder);
    833                 }
    834                 return testViewHolder;
    835             }
    836         };
    837         setupByConfig(new Config(VERTICAL, false, false).itemCount(300)
    838                 .adapter(adapter), true);
    839 
    840         final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
    841             @Override
    842             public void putRecycledView(RecyclerView.ViewHolder scrap) {
    843                 super.putRecycledView(scrap);
    844                 int cnt = childCount.decrementAndGet();
    845                 if (DEBUG) {
    846                     Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap);
    847                 }
    848             }
    849 
    850             @Override
    851             public RecyclerView.ViewHolder getRecycledView(int viewType) {
    852                 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
    853                 if (recycledView != null) {
    854                     final int cnt = childCount.incrementAndGet();
    855                     if (DEBUG) {
    856                         Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView);
    857                     }
    858                 }
    859                 return recycledView;
    860             }
    861         };
    862         pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500);
    863         mRecyclerView.setRecycledViewPool(pool);
    864 
    865 
    866         // now keep adding children to trigger more children being created etc.
    867         for (int i = 0; i < 100; i ++) {
    868             adapter.addAndNotify(15, 1);
    869             Thread.sleep(15);
    870         }
    871         getInstrumentation().waitForIdleSync();
    872         waitForAnimations(2);
    873         assertEquals("Children count should add up", childCount.get(),
    874                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
    875 
    876         // now trigger lots of add again, followed by a scroll to position
    877         for (int i = 0; i < 100; i ++) {
    878             adapter.addAndNotify(5 + (i % 3) * 3, 1);
    879             Thread.sleep(25);
    880         }
    881         smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
    882         waitForAnimations(2);
    883         getInstrumentation().waitForIdleSync();
    884         assertEquals("Children count should add up", childCount.get(),
    885                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
    886     }
    887 
    888 
    889     @Test
    890     public void dontRecycleChildrenOnDetach() throws Throwable {
    891         setupByConfig(new Config().recycleChildrenOnDetach(false), true);
    892         mActivityRule.runOnUiThread(new Runnable() {
    893             @Override
    894             public void run() {
    895                 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
    896                 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView);
    897                 assertEquals("No views are recycled", recyclerSize,
    898                         mRecyclerView.mRecycler.getRecycledViewPool().size());
    899             }
    900         });
    901     }
    902 
    903     @Test
    904     public void recycleChildrenOnDetach() throws Throwable {
    905         setupByConfig(new Config().recycleChildrenOnDetach(true), true);
    906         final int childCount = mLayoutManager.getChildCount();
    907         mActivityRule.runOnUiThread(new Runnable() {
    908             @Override
    909             public void run() {
    910                 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
    911                 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews(
    912                         mTestAdapter.getItemViewType(0), recyclerSize + childCount);
    913                 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView);
    914                 assertEquals("All children should be recycled", childCount + recyclerSize,
    915                         mRecyclerView.mRecycler.getRecycledViewPool().size());
    916             }
    917         });
    918     }
    919 
    920     @Test
    921     public void scrollAndClear() throws Throwable {
    922         setupByConfig(new Config(), true);
    923 
    924         assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
    925 
    926         mLayoutManager.expectLayouts(1);
    927         mActivityRule.runOnUiThread(new Runnable() {
    928             @Override
    929             public void run() {
    930                 mLayoutManager.scrollToPositionWithOffset(1, 0);
    931                 mTestAdapter.clearOnUIThread();
    932             }
    933         });
    934         mLayoutManager.waitForLayout(2);
    935 
    936         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
    937     }
    938 
    939 
    940     @Test
    941     public void accessibilityPositions() throws Throwable {
    942         setupByConfig(new Config(VERTICAL, false, false), true);
    943         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
    944                 .getCompatAccessibilityDelegate();
    945         final AccessibilityEvent event = AccessibilityEvent.obtain();
    946         mActivityRule.runOnUiThread(new Runnable() {
    947             @Override
    948             public void run() {
    949                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
    950             }
    951         });
    952         assertEquals("result should have first position",
    953                 event.getFromIndex(),
    954                 mLayoutManager.findFirstVisibleItemPosition());
    955         assertEquals("result should have last position",
    956                 event.getToIndex(),
    957                 mLayoutManager.findLastVisibleItemPosition());
    958     }
    959 }
    960