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 androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
     20 
     21 import static org.junit.Assert.assertEquals;
     22 import static org.junit.Assert.assertFalse;
     23 import static org.junit.Assert.assertNotNull;
     24 import static org.junit.Assert.assertNotSame;
     25 import static org.junit.Assert.assertNull;
     26 import static org.junit.Assert.assertSame;
     27 import static org.junit.Assert.assertThat;
     28 import static org.junit.Assert.assertTrue;
     29 
     30 import static java.util.concurrent.TimeUnit.SECONDS;
     31 
     32 import android.app.Instrumentation;
     33 import android.graphics.Rect;
     34 import android.os.Looper;
     35 import android.support.test.InstrumentationRegistry;
     36 import android.support.test.rule.ActivityTestRule;
     37 import android.util.Log;
     38 import android.view.LayoutInflater;
     39 import android.view.View;
     40 import android.view.ViewGroup;
     41 import android.widget.FrameLayout;
     42 import android.widget.TextView;
     43 
     44 import androidx.annotation.NonNull;
     45 import androidx.annotation.Nullable;
     46 import androidx.core.view.ViewCompat;
     47 import androidx.recyclerview.test.R;
     48 import androidx.testutils.PollingCheck;
     49 
     50 import org.hamcrest.CoreMatchers;
     51 import org.hamcrest.MatcherAssert;
     52 import org.junit.After;
     53 import org.junit.Before;
     54 import org.junit.Rule;
     55 
     56 import java.lang.reflect.InvocationTargetException;
     57 import java.lang.reflect.Method;
     58 import java.util.ArrayList;
     59 import java.util.HashSet;
     60 import java.util.List;
     61 import java.util.Set;
     62 import java.util.concurrent.CountDownLatch;
     63 import java.util.concurrent.TimeUnit;
     64 import java.util.concurrent.atomic.AtomicBoolean;
     65 import java.util.concurrent.atomic.AtomicInteger;
     66 
     67 abstract public class BaseRecyclerViewInstrumentationTest {
     68 
     69     private static final String TAG = "RecyclerViewTest";
     70 
     71     private boolean mDebug;
     72 
     73     protected RecyclerView mRecyclerView;
     74 
     75     protected AdapterHelper mAdapterHelper;
     76 
     77     private Throwable mMainThreadException;
     78 
     79     private boolean mIgnoreMainThreadException = false;
     80 
     81     Thread mInstrumentationThread;
     82 
     83     @Rule
     84     public ActivityTestRule<TestActivity> mActivityRule =
     85             new ActivityTestRule<>(TestActivity.class);
     86 
     87     public BaseRecyclerViewInstrumentationTest() {
     88         this(false);
     89     }
     90 
     91     public BaseRecyclerViewInstrumentationTest(boolean debug) {
     92         mDebug = debug;
     93     }
     94 
     95     void checkForMainThreadException() throws Throwable {
     96         if (!mIgnoreMainThreadException && mMainThreadException != null) {
     97             throw mMainThreadException;
     98         }
     99     }
    100 
    101     public void setIgnoreMainThreadException(boolean ignoreMainThreadException) {
    102         mIgnoreMainThreadException = ignoreMainThreadException;
    103     }
    104 
    105     public Throwable getMainThreadException() {
    106         return mMainThreadException;
    107     }
    108 
    109     protected TestActivity getActivity() {
    110         return mActivityRule.getActivity();
    111     }
    112 
    113     @Before
    114     public final void setUpInsThread() throws Exception {
    115         mInstrumentationThread = Thread.currentThread();
    116         Item.idCounter.set(0);
    117     }
    118 
    119     void setHasTransientState(final View view, final boolean value) {
    120         try {
    121             mActivityRule.runOnUiThread(new Runnable() {
    122                 @Override
    123                 public void run() {
    124                     ViewCompat.setHasTransientState(view, value);
    125                 }
    126             });
    127         } catch (Throwable throwable) {
    128             Log.e(TAG, "", throwable);
    129         }
    130     }
    131 
    132     public boolean canReUseActivity() {
    133         return true;
    134     }
    135 
    136     protected void enableAccessibility()
    137             throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    138         Method getUIAutomation = Instrumentation.class.getMethod("getUiAutomation");
    139         getUIAutomation.invoke(InstrumentationRegistry.getInstrumentation());
    140     }
    141 
    142     void setAdapter(final RecyclerView.Adapter adapter) throws Throwable {
    143         mActivityRule.runOnUiThread(new Runnable() {
    144             @Override
    145             public void run() {
    146                 mRecyclerView.setAdapter(adapter);
    147             }
    148         });
    149     }
    150 
    151     public View focusSearch(final View focused, final int direction) throws Throwable {
    152         return focusSearch(focused, direction, false);
    153     }
    154 
    155     public View focusSearch(final View focused, final int direction, boolean waitForScroll)
    156             throws Throwable {
    157         final View[] result = new View[1];
    158         mActivityRule.runOnUiThread(new Runnable() {
    159             @Override
    160             public void run() {
    161                 View view = focused.focusSearch(direction);
    162                 if (view != null && view != focused) {
    163                     view.requestFocus();
    164                 }
    165                 result[0] = view;
    166             }
    167         });
    168         if (waitForScroll && (result[0] != null)) {
    169             waitForIdleScroll(mRecyclerView);
    170         }
    171         return result[0];
    172     }
    173 
    174     protected WrappedRecyclerView inflateWrappedRV() {
    175         return (WrappedRecyclerView)
    176                 LayoutInflater.from(getActivity()).inflate(R.layout.wrapped_test_rv,
    177                         getRecyclerViewContainer(), false);
    178     }
    179 
    180     void swapAdapter(final RecyclerView.Adapter adapter,
    181             final boolean removeAndRecycleExistingViews) throws Throwable {
    182         mActivityRule.runOnUiThread(new Runnable() {
    183             @Override
    184             public void run() {
    185                 try {
    186                     mRecyclerView.swapAdapter(adapter, removeAndRecycleExistingViews);
    187                 } catch (Throwable t) {
    188                     postExceptionToInstrumentation(t);
    189                 }
    190             }
    191         });
    192         checkForMainThreadException();
    193     }
    194 
    195     void postExceptionToInstrumentation(Throwable t) {
    196         if (mInstrumentationThread == Thread.currentThread()) {
    197             throw new RuntimeException(t);
    198         }
    199         if (mMainThreadException != null) {
    200             Log.e(TAG, "receiving another main thread exception. dropping.", t);
    201         } else {
    202             Log.e(TAG, "captured exception on main thread", t);
    203             mMainThreadException = t;
    204         }
    205 
    206         if (mRecyclerView != null && mRecyclerView
    207                 .getLayoutManager() instanceof TestLayoutManager) {
    208             TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager();
    209             // finish all layouts so that we get the correct exception
    210             if (lm.layoutLatch != null) {
    211                 while (lm.layoutLatch.getCount() > 0) {
    212                     lm.layoutLatch.countDown();
    213                 }
    214             }
    215         }
    216     }
    217 
    218     public Instrumentation getInstrumentation() {
    219         return InstrumentationRegistry.getInstrumentation();
    220     }
    221 
    222     @After
    223     public final void tearDown() throws Exception {
    224         if (mRecyclerView != null) {
    225             try {
    226                 removeRecyclerView();
    227             } catch (Throwable throwable) {
    228                 throwable.printStackTrace();
    229             }
    230         }
    231         getInstrumentation().waitForIdleSync();
    232 
    233         try {
    234             checkForMainThreadException();
    235         } catch (Exception e) {
    236             throw e;
    237         } catch (Throwable throwable) {
    238             throw new Exception(Log.getStackTraceString(throwable));
    239         }
    240     }
    241 
    242     public Rect getDecoratedRecyclerViewBounds() {
    243         return new Rect(
    244                 mRecyclerView.getPaddingLeft(),
    245                 mRecyclerView.getPaddingTop(),
    246                 mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(),
    247                 mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()
    248         );
    249     }
    250 
    251     public void removeRecyclerView() throws Throwable {
    252         if (mRecyclerView == null) {
    253             return;
    254         }
    255         if (!isMainThread()) {
    256             getInstrumentation().waitForIdleSync();
    257         }
    258         mActivityRule.runOnUiThread(new Runnable() {
    259             @Override
    260             public void run() {
    261                 try {
    262                     // do not run validation if we already have an error
    263                     if (mMainThreadException == null) {
    264                         final RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
    265                         if (adapter instanceof AttachDetachCountingAdapter) {
    266                             ((AttachDetachCountingAdapter) adapter).getCounter()
    267                                     .validateRemaining(mRecyclerView);
    268                         }
    269                     }
    270                     getActivity().getContainer().removeAllViews();
    271                 } catch (Throwable t) {
    272                     postExceptionToInstrumentation(t);
    273                 }
    274             }
    275         });
    276         mRecyclerView = null;
    277     }
    278 
    279     void waitForAnimations(int seconds) throws Throwable {
    280         final CountDownLatch latch = new CountDownLatch(1);
    281         mActivityRule.runOnUiThread(new Runnable() {
    282             @Override
    283             public void run() {
    284                 mRecyclerView.mItemAnimator
    285                         .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
    286                             @Override
    287                             public void onAnimationsFinished() {
    288                                 latch.countDown();
    289                             }
    290                         });
    291             }
    292         });
    293 
    294         assertTrue("animations didn't finish on expected time of " + seconds + " seconds",
    295                 latch.await(seconds, TimeUnit.SECONDS));
    296     }
    297 
    298     public void waitForIdleScroll(final RecyclerView recyclerView) throws Throwable {
    299         final CountDownLatch latch = new CountDownLatch(1);
    300         mActivityRule.runOnUiThread(new Runnable() {
    301             @Override
    302             public void run() {
    303                 RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() {
    304                     @Override
    305                     public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    306                         if (newState == SCROLL_STATE_IDLE) {
    307                             latch.countDown();
    308                             recyclerView.removeOnScrollListener(this);
    309                         }
    310                     }
    311                 };
    312                 if (recyclerView.getScrollState() == SCROLL_STATE_IDLE) {
    313                     latch.countDown();
    314                 } else {
    315                     recyclerView.addOnScrollListener(listener);
    316                 }
    317             }
    318         });
    319         assertTrue("should go idle in 10 seconds", latch.await(10, TimeUnit.SECONDS));
    320     }
    321 
    322     public boolean requestFocus(final View view, boolean waitForScroll) throws Throwable {
    323         final boolean[] result = new boolean[1];
    324         mActivityRule.runOnUiThread(new Runnable() {
    325             @Override
    326             public void run() {
    327                 result[0] = view.requestFocus();
    328             }
    329         });
    330         PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
    331             @Override
    332             public boolean canProceed() {
    333                 return view.hasFocus();
    334             }
    335         });
    336         if (waitForScroll && result[0]) {
    337             waitForIdleScroll(mRecyclerView);
    338         }
    339         return result[0];
    340     }
    341 
    342     public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
    343         setRecyclerView(recyclerView, true);
    344     }
    345     public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool)
    346             throws Throwable {
    347         setRecyclerView(recyclerView, assignDummyPool, true);
    348     }
    349     public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool,
    350             boolean addPositionCheckItemAnimator)
    351             throws Throwable {
    352         mRecyclerView = recyclerView;
    353         if (assignDummyPool) {
    354             RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
    355                 @Override
    356                 public RecyclerView.ViewHolder getRecycledView(int viewType) {
    357                     RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType);
    358                     if (viewHolder == null) {
    359                         return null;
    360                     }
    361                     viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND);
    362                     viewHolder.mPosition = 200;
    363                     viewHolder.mOldPosition = 300;
    364                     viewHolder.mPreLayoutPosition = 500;
    365                     return viewHolder;
    366                 }
    367 
    368                 @Override
    369                 public void putRecycledView(RecyclerView.ViewHolder scrap) {
    370                     assertNull(scrap.mOwnerRecyclerView);
    371                     super.putRecycledView(scrap);
    372                 }
    373             };
    374             mRecyclerView.setRecycledViewPool(pool);
    375         }
    376         if (addPositionCheckItemAnimator) {
    377             mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
    378                 @Override
    379                 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
    380                         RecyclerView.State state) {
    381                     RecyclerView.ViewHolder vh = parent.getChildViewHolder(view);
    382                     if (!vh.isRemoved()) {
    383                         assertNotSame("If getItemOffsets is called, child should have a valid"
    384                                         + " adapter position unless it is removed : " + vh,
    385                                 vh.getAdapterPosition(), RecyclerView.NO_POSITION);
    386                     }
    387                 }
    388             });
    389         }
    390         mAdapterHelper = recyclerView.mAdapterHelper;
    391         mActivityRule.runOnUiThread(new Runnable() {
    392             @Override
    393             public void run() {
    394                 getActivity().getContainer().addView(recyclerView);
    395             }
    396         });
    397     }
    398 
    399     protected FrameLayout getRecyclerViewContainer() {
    400         return getActivity().getContainer();
    401     }
    402 
    403     protected void requestLayoutOnUIThread(final View view) throws Throwable {
    404         mActivityRule.runOnUiThread(new Runnable() {
    405             @Override
    406             public void run() {
    407                 view.requestLayout();
    408             }
    409         });
    410     }
    411 
    412     protected void scrollBy(final int dt) throws Throwable {
    413         mActivityRule.runOnUiThread(new Runnable() {
    414             @Override
    415             public void run() {
    416                 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
    417                     mRecyclerView.scrollBy(dt, 0);
    418                 } else {
    419                     mRecyclerView.scrollBy(0, dt);
    420                 }
    421 
    422             }
    423         });
    424     }
    425 
    426     protected void smoothScrollBy(final int dt) throws Throwable {
    427         mActivityRule.runOnUiThread(new Runnable() {
    428             @Override
    429             public void run() {
    430                 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
    431                     mRecyclerView.smoothScrollBy(dt, 0);
    432                 } else {
    433                     mRecyclerView.smoothScrollBy(0, dt);
    434                 }
    435 
    436             }
    437         });
    438         getInstrumentation().waitForIdleSync();
    439     }
    440 
    441     void scrollToPosition(final int position) throws Throwable {
    442         mActivityRule.runOnUiThread(new Runnable() {
    443             @Override
    444             public void run() {
    445                 mRecyclerView.getLayoutManager().scrollToPosition(position);
    446             }
    447         });
    448     }
    449 
    450     void smoothScrollToPosition(final int position) throws Throwable {
    451         smoothScrollToPosition(position, true);
    452     }
    453 
    454     void smoothScrollToPosition(final int position, boolean assertArrival) throws Throwable {
    455         if (mDebug) {
    456             Log.d(TAG, "SMOOTH scrolling to " + position);
    457         }
    458         final CountDownLatch viewAdded = new CountDownLatch(1);
    459         final RecyclerView.OnChildAttachStateChangeListener listener =
    460                 new RecyclerView.OnChildAttachStateChangeListener() {
    461                     @Override
    462                     public void onChildViewAttachedToWindow(@NonNull View view) {
    463                         if (position == mRecyclerView.getChildAdapterPosition(view)) {
    464                             viewAdded.countDown();
    465                         }
    466                     }
    467                     @Override
    468                     public void onChildViewDetachedFromWindow(@NonNull View view) {
    469                     }
    470                 };
    471         final AtomicBoolean addedListener = new AtomicBoolean(false);
    472         mActivityRule.runOnUiThread(new Runnable() {
    473             @Override
    474             public void run() {
    475                 RecyclerView.ViewHolder viewHolderForAdapterPosition =
    476                         mRecyclerView.findViewHolderForAdapterPosition(position);
    477                 if (viewHolderForAdapterPosition != null) {
    478                     viewAdded.countDown();
    479                 } else {
    480                     mRecyclerView.addOnChildAttachStateChangeListener(listener);
    481                     addedListener.set(true);
    482                 }
    483 
    484             }
    485         });
    486         mActivityRule.runOnUiThread(new Runnable() {
    487             @Override
    488             public void run() {
    489                 mRecyclerView.smoothScrollToPosition(position);
    490             }
    491         });
    492         getInstrumentation().waitForIdleSync();
    493         assertThat("should be able to scroll in 10 seconds", !assertArrival ||
    494                         viewAdded.await(10, TimeUnit.SECONDS),
    495                 CoreMatchers.is(true));
    496         waitForIdleScroll(mRecyclerView);
    497         if (mDebug) {
    498             Log.d(TAG, "SMOOTH scrolling done");
    499         }
    500         if (addedListener.get()) {
    501             mActivityRule.runOnUiThread(new Runnable() {
    502                 @Override
    503                 public void run() {
    504                     mRecyclerView.removeOnChildAttachStateChangeListener(listener);
    505                 }
    506             });
    507         }
    508         getInstrumentation().waitForIdleSync();
    509     }
    510 
    511     void freezeLayout(final boolean freeze) throws Throwable {
    512         mActivityRule.runOnUiThread(new Runnable() {
    513             @Override
    514             public void run() {
    515                 mRecyclerView.setLayoutFrozen(freeze);
    516             }
    517         });
    518     }
    519 
    520     public void setVisibility(final View view, final int visibility) throws Throwable {
    521         mActivityRule.runOnUiThread(new Runnable() {
    522             @Override
    523             public void run() {
    524                 view.setVisibility(visibility);
    525             }
    526         });
    527     }
    528 
    529     public class TestViewHolder extends RecyclerView.ViewHolder {
    530 
    531         Item mBoundItem;
    532         Object mData;
    533 
    534         public TestViewHolder(View itemView) {
    535             super(itemView);
    536             itemView.setFocusable(true);
    537         }
    538 
    539         @Override
    540         public String toString() {
    541             return super.toString() + " item:" + mBoundItem + ", data:" + mData;
    542         }
    543 
    544         public Object getData() {
    545             return mData;
    546         }
    547 
    548         public void setData(Object data) {
    549             mData = data;
    550         }
    551     }
    552     class DumbLayoutManager extends TestLayoutManager {
    553         @Override
    554         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    555             detachAndScrapAttachedViews(recycler);
    556             layoutRange(recycler, 0, state.getItemCount());
    557             if (layoutLatch != null) {
    558                 layoutLatch.countDown();
    559             }
    560         }
    561     }
    562 
    563     public class TestLayoutManager extends RecyclerView.LayoutManager {
    564         int mScrollVerticallyAmount;
    565         int mScrollHorizontallyAmount;
    566         protected CountDownLatch layoutLatch;
    567         private boolean mSupportsPredictive = false;
    568 
    569         public void expectLayouts(int count) {
    570             layoutLatch = new CountDownLatch(count);
    571         }
    572 
    573         public void waitForLayout(int seconds) throws Throwable {
    574             layoutLatch.await(seconds * (mDebug ? 1000 : 1), SECONDS);
    575             checkForMainThreadException();
    576             MatcherAssert.assertThat("all layouts should complete on time",
    577                     layoutLatch.getCount(), CoreMatchers.is(0L));
    578             // use a runnable to ensure RV layout is finished
    579             getInstrumentation().runOnMainSync(new Runnable() {
    580                 @Override
    581                 public void run() {
    582                 }
    583             });
    584         }
    585 
    586         public boolean isSupportsPredictive() {
    587             return mSupportsPredictive;
    588         }
    589 
    590         public void setSupportsPredictive(boolean supportsPredictive) {
    591             mSupportsPredictive = supportsPredictive;
    592         }
    593 
    594         @Override
    595         public boolean supportsPredictiveItemAnimations() {
    596             return mSupportsPredictive;
    597         }
    598 
    599         public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
    600             layoutLatch.await(timeout, TimeUnit.SECONDS);
    601             assertEquals(msg, count, layoutLatch.getCount());
    602         }
    603 
    604         public void assertNoLayout(String msg, long timeout) throws Throwable {
    605             layoutLatch.await(timeout, TimeUnit.SECONDS);
    606             assertFalse(msg, layoutLatch.getCount() == 0);
    607         }
    608 
    609         @Override
    610         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    611             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    612                     ViewGroup.LayoutParams.WRAP_CONTENT);
    613         }
    614 
    615         void assertVisibleItemPositions() {
    616             int i = getChildCount();
    617             TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter();
    618             while (i-- > 0) {
    619                 View view = getChildAt(i);
    620                 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
    621                 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem;
    622                 if (mDebug) {
    623                     Log.d(TAG, "testing item " + i);
    624                 }
    625                 if (!lp.isItemRemoved()) {
    626                     RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view);
    627                     assertSame("item position in LP should match adapter value :" + vh,
    628                             testAdapter.mItems.get(vh.mPosition), item);
    629                 }
    630             }
    631         }
    632 
    633         RecyclerView.LayoutParams getLp(View v) {
    634             return (RecyclerView.LayoutParams) v.getLayoutParams();
    635         }
    636 
    637         protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) {
    638             assertScrap(recycler);
    639             if (mDebug) {
    640                 Log.d(TAG, "will layout items from " + start + " to " + end);
    641             }
    642             int diff = end > start ? 1 : -1;
    643             int top = 0;
    644             for (int i = start; i != end; i+=diff) {
    645                 if (mDebug) {
    646                     Log.d(TAG, "laying out item " + i);
    647                 }
    648                 View view = recycler.getViewForPosition(i);
    649                 assertNotNull("view should not be null for valid position. "
    650                         + "got null view at position " + i, view);
    651                 if (!mRecyclerView.mState.isPreLayout()) {
    652                     RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view
    653                             .getLayoutParams();
    654                     assertFalse("In post layout, getViewForPosition should never return a view "
    655                             + "that is removed", layoutParams != null
    656                             && layoutParams.isItemRemoved());
    657 
    658                 }
    659                 assertEquals("getViewForPosition should return correct position",
    660                         i, getPosition(view));
    661                 addView(view);
    662                 measureChildWithMargins(view, 0, 0);
    663                 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
    664                     layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top,
    665                             getWidth(), top + getDecoratedMeasuredHeight(view));
    666                 } else {
    667                     layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view)
    668                             , top + getDecoratedMeasuredHeight(view));
    669                 }
    670 
    671                 top += view.getMeasuredHeight();
    672             }
    673         }
    674 
    675         private void assertScrap(RecyclerView.Recycler recycler) {
    676             if (mRecyclerView.getAdapter() != null &&
    677                     !mRecyclerView.getAdapter().hasStableIds()) {
    678                 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) {
    679                     assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid());
    680                 }
    681             }
    682         }
    683 
    684         @Override
    685         public boolean canScrollHorizontally() {
    686             return true;
    687         }
    688 
    689         @Override
    690         public boolean canScrollVertically() {
    691             return true;
    692         }
    693 
    694         @Override
    695         public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
    696                 RecyclerView.State state) {
    697             mScrollHorizontallyAmount += dx;
    698             return dx;
    699         }
    700 
    701         @Override
    702         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
    703                 RecyclerView.State state) {
    704             mScrollVerticallyAmount += dy;
    705             return dy;
    706         }
    707 
    708         // START MOCKITO OVERRIDES
    709         // We override package protected methods to make them public. This is necessary to run
    710         // mockito on Kitkat
    711         @Override
    712         public void setRecyclerView(RecyclerView recyclerView) {
    713             super.setRecyclerView(recyclerView);
    714         }
    715 
    716         @Override
    717         public void dispatchAttachedToWindow(RecyclerView view) {
    718             super.dispatchAttachedToWindow(view);
    719         }
    720 
    721         @Override
    722         public void dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
    723             super.dispatchDetachedFromWindow(view, recycler);
    724         }
    725 
    726         @Override
    727         public void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
    728             super.setExactMeasureSpecsFrom(recyclerView);
    729         }
    730 
    731         @Override
    732         public void setMeasureSpecs(int wSpec, int hSpec) {
    733             super.setMeasureSpecs(wSpec, hSpec);
    734         }
    735 
    736         @Override
    737         public void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
    738             super.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    739         }
    740 
    741         @Override
    742         public boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec,
    743                 RecyclerView.LayoutParams lp) {
    744             return super.shouldReMeasureChild(child, widthSpec, heightSpec, lp);
    745         }
    746 
    747         @Override
    748         public boolean shouldMeasureChild(View child, int widthSpec, int heightSpec,
    749                 RecyclerView.LayoutParams lp) {
    750             return super.shouldMeasureChild(child, widthSpec, heightSpec, lp);
    751         }
    752 
    753         @Override
    754         public void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) {
    755             super.removeAndRecycleScrapInt(recycler);
    756         }
    757 
    758         @Override
    759         public void stopSmoothScroller() {
    760             super.stopSmoothScroller();
    761         }
    762 
    763         // END MOCKITO OVERRIDES
    764     }
    765 
    766     static class Item {
    767         final static AtomicInteger idCounter = new AtomicInteger(0);
    768         final public int mId = idCounter.incrementAndGet();
    769 
    770         int mAdapterIndex;
    771 
    772         String mText;
    773         int mType = 0;
    774         boolean mFocusable;
    775 
    776         Item(int adapterIndex, String text) {
    777             mAdapterIndex = adapterIndex;
    778             mText = text;
    779             mFocusable = true;
    780         }
    781 
    782         public boolean isFocusable() {
    783             return mFocusable;
    784         }
    785 
    786         public void setFocusable(boolean mFocusable) {
    787             this.mFocusable = mFocusable;
    788         }
    789 
    790         @Override
    791         public String toString() {
    792             return "Item{" +
    793                     "mId=" + mId +
    794                     ", originalIndex=" + mAdapterIndex +
    795                     ", text='" + mText + '\'' +
    796                     '}';
    797         }
    798     }
    799 
    800     public class FocusableAdapter extends RecyclerView.Adapter<TestViewHolder> {
    801 
    802         private int mCount;
    803 
    804         FocusableAdapter(int count) {
    805             mCount = count;
    806         }
    807 
    808         @Override
    809         public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    810             final TextView textView = new TextView(parent.getContext());
    811             textView.setLayoutParams(new ViewGroup.LayoutParams(
    812                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    813             textView.setFocusable(true);
    814             textView.setBackgroundResource(R.drawable.item_bg);
    815             return new TestViewHolder(textView);
    816         }
    817 
    818         @Override
    819         public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
    820             ((TextView) holder.itemView).setText("Item " + position);
    821         }
    822 
    823         @Override
    824         public int getItemCount() {
    825             return mCount;
    826         }
    827     }
    828 
    829     public class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
    830             implements AttachDetachCountingAdapter {
    831 
    832         public static final String DEFAULT_ITEM_PREFIX = "Item ";
    833 
    834         ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter();
    835         List<Item> mItems;
    836         final @Nullable RecyclerView.LayoutParams mLayoutParams;
    837 
    838         public TestAdapter(int count) {
    839             this(count, null);
    840         }
    841 
    842         public TestAdapter(int count, @Nullable RecyclerView.LayoutParams layoutParams) {
    843             mItems = new ArrayList<Item>(count);
    844             addItems(0, count, DEFAULT_ITEM_PREFIX);
    845             mLayoutParams = layoutParams;
    846         }
    847 
    848         void addItems(int pos, int count, String prefix) {
    849             for (int i = 0; i < count; i++, pos++) {
    850                 mItems.add(pos, new Item(pos, prefix));
    851             }
    852         }
    853 
    854         @Override
    855         public int getItemViewType(int position) {
    856             return getItemAt(position).mType;
    857         }
    858 
    859         @Override
    860         public void onViewAttachedToWindow(TestViewHolder holder) {
    861             super.onViewAttachedToWindow(holder);
    862             mAttachmentCounter.onViewAttached(holder);
    863         }
    864 
    865         @Override
    866         public void onViewDetachedFromWindow(TestViewHolder holder) {
    867             super.onViewDetachedFromWindow(holder);
    868             mAttachmentCounter.onViewDetached(holder);
    869         }
    870 
    871         @Override
    872         public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    873             super.onAttachedToRecyclerView(recyclerView);
    874             mAttachmentCounter.onAttached(recyclerView);
    875         }
    876 
    877         @Override
    878         public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
    879             super.onDetachedFromRecyclerView(recyclerView);
    880             mAttachmentCounter.onDetached(recyclerView);
    881         }
    882 
    883         @Override
    884         public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    885                 int viewType) {
    886             TextView itemView = new TextView(parent.getContext());
    887             itemView.setFocusableInTouchMode(true);
    888             itemView.setFocusable(true);
    889             return new TestViewHolder(itemView);
    890         }
    891 
    892         @Override
    893         public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
    894             assertNotNull(holder.mOwnerRecyclerView);
    895             assertEquals(position, holder.getAdapterPosition());
    896             final Item item = mItems.get(position);
    897             ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")");
    898             holder.mBoundItem = item;
    899             if (mLayoutParams != null) {
    900                 holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(mLayoutParams));
    901             }
    902         }
    903 
    904         public Item getItemAt(int position) {
    905             return mItems.get(position);
    906         }
    907 
    908         @Override
    909         public void onViewRecycled(@NonNull TestViewHolder holder) {
    910             super.onViewRecycled(holder);
    911             final int adapterPosition = holder.getAdapterPosition();
    912             final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() &&
    913                     !holder.isAdapterPositionUnknown() && !holder.isInvalid();
    914             String log = "Position check for " + holder.toString();
    915             assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION);
    916             if (shouldHavePosition) {
    917                 assertTrue(log, mItems.size() > adapterPosition);
    918                 // TODO: fix b/36042615 getAdapterPosition() is wrong in
    919                 // consumePendingUpdatesInOnePass where it applies pending change to already
    920                 // modified position.
    921                 if (holder.mPreLayoutPosition == RecyclerView.NO_POSITION) {
    922                     assertSame(log, holder.mBoundItem, mItems.get(adapterPosition));
    923                 }
    924             }
    925         }
    926 
    927         public void deleteAndNotify(final int start, final int count) throws Throwable {
    928             deleteAndNotify(new int[]{start, count});
    929         }
    930 
    931         /**
    932          * Deletes items in the given ranges.
    933          * <p>
    934          * Note that each operation affects the one after so you should offset them properly.
    935          * <p>
    936          * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with
    937          * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be
    938          * A D E. Then it will delete 2,1 which means it will delete E.
    939          */
    940         public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
    941             for (int[] tuple : startCountTuples) {
    942                 tuple[1] = -tuple[1];
    943             }
    944             mActivityRule.runOnUiThread(new AddRemoveRunnable(startCountTuples));
    945         }
    946 
    947         @Override
    948         public long getItemId(int position) {
    949             return hasStableIds() ? mItems.get(position).mId : super.getItemId(position);
    950         }
    951 
    952         public void offsetOriginalIndices(int start, int offset) {
    953             for (int i = start; i < mItems.size(); i++) {
    954                 mItems.get(i).mAdapterIndex += offset;
    955             }
    956         }
    957 
    958         /**
    959          * @param start inclusive
    960          * @param end exclusive
    961          * @param offset
    962          */
    963         public void offsetOriginalIndicesBetween(int start, int end, int offset) {
    964             for (int i = start; i < end && i < mItems.size(); i++) {
    965                 mItems.get(i).mAdapterIndex += offset;
    966             }
    967         }
    968 
    969         public void addAndNotify(final int count) throws Throwable {
    970             assertEquals(0, mItems.size());
    971             mActivityRule.runOnUiThread(
    972                     new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}));
    973         }
    974 
    975         public void resetItemsTo(final List<Item> testItems) throws Throwable {
    976             if (!mItems.isEmpty()) {
    977                 deleteAndNotify(0, mItems.size());
    978             }
    979             mItems = testItems;
    980             mActivityRule.runOnUiThread(new Runnable() {
    981                 @Override
    982                 public void run() {
    983                     notifyItemRangeInserted(0, testItems.size());
    984                 }
    985             });
    986         }
    987 
    988         public void addAndNotify(final int start, final int count) throws Throwable {
    989             addAndNotify(new int[]{start, count});
    990         }
    991 
    992         public void addAndNotify(final int[]... startCountTuples) throws Throwable {
    993             mActivityRule.runOnUiThread(new AddRemoveRunnable(startCountTuples));
    994         }
    995 
    996         public void dispatchDataSetChanged() throws Throwable {
    997             mActivityRule.runOnUiThread(new Runnable() {
    998                 @Override
    999                 public void run() {
   1000                     notifyDataSetChanged();
   1001                 }
   1002             });
   1003         }
   1004 
   1005         public void changeAndNotify(final int start, final int count) throws Throwable {
   1006             mActivityRule.runOnUiThread(new Runnable() {
   1007                 @Override
   1008                 public void run() {
   1009                     notifyItemRangeChanged(start, count);
   1010                 }
   1011             });
   1012         }
   1013 
   1014         public void changeAndNotifyWithPayload(final int start, final int count,
   1015                 final Object payload) throws Throwable {
   1016             mActivityRule.runOnUiThread(new Runnable() {
   1017                 @Override
   1018                 public void run() {
   1019                     notifyItemRangeChanged(start, count, payload);
   1020                 }
   1021             });
   1022         }
   1023 
   1024         public void changePositionsAndNotify(final int... positions) throws Throwable {
   1025             mActivityRule.runOnUiThread(new Runnable() {
   1026                 @Override
   1027                 public void run() {
   1028                     for (int i = 0; i < positions.length; i += 1) {
   1029                         TestAdapter.super.notifyItemRangeChanged(positions[i], 1);
   1030                     }
   1031                 }
   1032             });
   1033         }
   1034 
   1035         /**
   1036          * Similar to other methods but negative count means delete and position count means add.
   1037          * <p>
   1038          * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an
   1039          * item to index 1, then remove an item from index 2 (updated index 2)
   1040          */
   1041         public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable {
   1042             mActivityRule.runOnUiThread(new AddRemoveRunnable(startCountTuples));
   1043         }
   1044 
   1045         @Override
   1046         public int getItemCount() {
   1047             return mItems.size();
   1048         }
   1049 
   1050         /**
   1051          * Uses notifyDataSetChanged
   1052          */
   1053         public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable {
   1054             for (int i = 0; i < fromToTuples.length; i += 1) {
   1055                 int[] tuple = fromToTuples[i];
   1056                 moveItem(tuple[0], tuple[1], false);
   1057             }
   1058             if (notifyChange) {
   1059                 dispatchDataSetChanged();
   1060             }
   1061         }
   1062 
   1063         /**
   1064          * Uses notifyDataSetChanged
   1065          */
   1066         public void moveItem(final int from, final int to, final boolean notifyChange)
   1067                 throws Throwable {
   1068             mActivityRule.runOnUiThread(new Runnable() {
   1069                 @Override
   1070                 public void run() {
   1071                     moveInUIThread(from, to);
   1072                     if (notifyChange) {
   1073                         notifyDataSetChanged();
   1074                     }
   1075                 }
   1076             });
   1077         }
   1078 
   1079         /**
   1080          * Uses notifyItemMoved
   1081          */
   1082         public void moveAndNotify(final int from, final int to) throws Throwable {
   1083             mActivityRule.runOnUiThread(new Runnable() {
   1084                 @Override
   1085                 public void run() {
   1086                     moveInUIThread(from, to);
   1087                     notifyItemMoved(from, to);
   1088                 }
   1089             });
   1090         }
   1091 
   1092         void changeAllItemsAndNotifyDataSetChanged(int count) {
   1093             assertEquals("clearOnUIThread called from a wrong thread",
   1094                     Looper.getMainLooper(), Looper.myLooper());
   1095             mItems = new ArrayList<>();
   1096             addItems(0, count, DEFAULT_ITEM_PREFIX);
   1097             notifyDataSetChanged();
   1098         }
   1099 
   1100         public void clearOnUIThread() {
   1101             changeAllItemsAndNotifyDataSetChanged(0);
   1102         }
   1103 
   1104         protected void moveInUIThread(int from, int to) {
   1105             Item item = mItems.remove(from);
   1106             offsetOriginalIndices(from, -1);
   1107             mItems.add(to, item);
   1108             offsetOriginalIndices(to + 1, 1);
   1109             item.mAdapterIndex = to;
   1110         }
   1111 
   1112 
   1113         @Override
   1114         public ViewAttachDetachCounter getCounter() {
   1115             return mAttachmentCounter;
   1116         }
   1117 
   1118         private class AddRemoveRunnable implements Runnable {
   1119             final String mNewItemPrefix;
   1120             final int[][] mStartCountTuples;
   1121 
   1122             public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) {
   1123                 mNewItemPrefix = newItemPrefix;
   1124                 mStartCountTuples = startCountTuples;
   1125             }
   1126 
   1127             public AddRemoveRunnable(int[][] startCountTuples) {
   1128                 this("new item ", startCountTuples);
   1129             }
   1130 
   1131             @Override
   1132             public void run() {
   1133                 for (int[] tuple : mStartCountTuples) {
   1134                     if (tuple[1] < 0) {
   1135                         delete(tuple);
   1136                     } else {
   1137                         add(tuple);
   1138                     }
   1139                 }
   1140             }
   1141 
   1142             private void add(int[] tuple) {
   1143                 // offset others
   1144                 offsetOriginalIndices(tuple[0], tuple[1]);
   1145                 addItems(tuple[0], tuple[1], mNewItemPrefix);
   1146                 notifyItemRangeInserted(tuple[0], tuple[1]);
   1147             }
   1148 
   1149             private void delete(int[] tuple) {
   1150                 final int count = -tuple[1];
   1151                 offsetOriginalIndices(tuple[0] + count, tuple[1]);
   1152                 for (int i = 0; i < count; i++) {
   1153                     mItems.remove(tuple[0]);
   1154                 }
   1155                 notifyItemRangeRemoved(tuple[0], count);
   1156             }
   1157         }
   1158     }
   1159 
   1160     public boolean isMainThread() {
   1161         return Looper.myLooper() == Looper.getMainLooper();
   1162     }
   1163 
   1164     static class TargetTuple {
   1165 
   1166         final int mPosition;
   1167 
   1168         final int mLayoutDirection;
   1169 
   1170         TargetTuple(int position, int layoutDirection) {
   1171             this.mPosition = position;
   1172             this.mLayoutDirection = layoutDirection;
   1173         }
   1174 
   1175         @Override
   1176         public String toString() {
   1177             return "TargetTuple{" +
   1178                     "mPosition=" + mPosition +
   1179                     ", mLayoutDirection=" + mLayoutDirection +
   1180                     '}';
   1181         }
   1182     }
   1183 
   1184     public interface AttachDetachCountingAdapter {
   1185 
   1186         ViewAttachDetachCounter getCounter();
   1187     }
   1188 
   1189     public class ViewAttachDetachCounter {
   1190 
   1191         Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>();
   1192 
   1193         public void validateRemaining(RecyclerView recyclerView) {
   1194             final int childCount = recyclerView.getChildCount();
   1195             for (int i = 0; i < childCount; i++) {
   1196                 View view = recyclerView.getChildAt(i);
   1197                 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
   1198                 assertTrue("remaining view should be in attached set " + vh,
   1199                         mAttachedSet.contains(vh));
   1200             }
   1201             assertEquals("there should not be any views left in attached set",
   1202                     childCount, mAttachedSet.size());
   1203         }
   1204 
   1205         public void onViewDetached(RecyclerView.ViewHolder viewHolder) {
   1206             try {
   1207                 assertTrue("view holder should be in attached set",
   1208                         mAttachedSet.remove(viewHolder));
   1209             } catch (Throwable t) {
   1210                 postExceptionToInstrumentation(t);
   1211             }
   1212         }
   1213 
   1214         public void onViewAttached(RecyclerView.ViewHolder viewHolder) {
   1215             try {
   1216                 assertTrue("view holder should not be in attached set",
   1217                         mAttachedSet.add(viewHolder));
   1218             } catch (Throwable t) {
   1219                 postExceptionToInstrumentation(t);
   1220             }
   1221         }
   1222 
   1223         public void onAttached(RecyclerView recyclerView) {
   1224             // when a new RV is attached, clear the set and add all view holders
   1225             mAttachedSet.clear();
   1226             final int childCount = recyclerView.getChildCount();
   1227             for (int i = 0; i < childCount; i ++) {
   1228                 View view = recyclerView.getChildAt(i);
   1229                 mAttachedSet.add(recyclerView.getChildViewHolder(view));
   1230             }
   1231         }
   1232 
   1233         public void onDetached(RecyclerView recyclerView) {
   1234             validateRemaining(recyclerView);
   1235         }
   1236     }
   1237 
   1238 
   1239     public static View findFirstFullyVisibleChild(RecyclerView parent) {
   1240         for (int i = 0; i < parent.getChildCount(); i++) {
   1241             View child = parent.getChildAt(i);
   1242             if (isViewFullyInBound(parent, child)) {
   1243                 return child;
   1244             }
   1245         }
   1246         return null;
   1247     }
   1248 
   1249     public static View findLastFullyVisibleChild(RecyclerView parent) {
   1250         for (int i = parent.getChildCount() - 1; i >= 0; i--) {
   1251             View child = parent.getChildAt(i);
   1252             if (isViewFullyInBound(parent, child)) {
   1253                 return child;
   1254             }
   1255         }
   1256         return null;
   1257     }
   1258 
   1259     /**
   1260      * Returns whether a child of RecyclerView is partially in bound. A child is
   1261      * partially in-bounds if it's either fully or partially visible on the screen.
   1262      * @param parent The RecyclerView holding the child.
   1263      * @param child The child view to be checked whether is partially (or fully) within RV's bounds.
   1264      * @return True if the child view is partially (or fully) visible; false otherwise.
   1265      */
   1266     public static boolean isViewPartiallyInBound(RecyclerView parent, View child) {
   1267         if (child == null) {
   1268             return false;
   1269         }
   1270         final int parentLeft = parent.getPaddingLeft();
   1271         final int parentTop = parent.getPaddingTop();
   1272         final int parentRight = parent.getWidth() - parent.getPaddingRight();
   1273         final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
   1274 
   1275         final int childLeft = child.getLeft() - child.getScrollX();
   1276         final int childTop = child.getTop() - child.getScrollY();
   1277         final int childRight = child.getRight() - child.getScrollX();
   1278         final int childBottom = child.getBottom() - child.getScrollY();
   1279 
   1280         if (childLeft >= parentRight || childRight <= parentLeft
   1281                 || childTop >= parentBottom || childBottom <= parentTop) {
   1282             return false;
   1283         }
   1284         return true;
   1285     }
   1286 
   1287     /**
   1288      * Returns whether a child of RecyclerView is fully in-bounds, that is it's fully visible
   1289      * on the screen.
   1290      * @param parent The RecyclerView holding the child.
   1291      * @param child The child view to be checked whether is fully within RV's bounds.
   1292      * @return True if the child view is fully visible; false otherwise.
   1293      */
   1294     public static boolean isViewFullyInBound(RecyclerView parent, View child) {
   1295         if (child == null) {
   1296             return false;
   1297         }
   1298         final int parentLeft = parent.getPaddingLeft();
   1299         final int parentTop = parent.getPaddingTop();
   1300         final int parentRight = parent.getWidth() - parent.getPaddingRight();
   1301         final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
   1302 
   1303         final int childLeft = child.getLeft() - child.getScrollX();
   1304         final int childTop = child.getTop() - child.getScrollY();
   1305         final int childRight = child.getRight() - child.getScrollX();
   1306         final int childBottom = child.getBottom() - child.getScrollY();
   1307 
   1308         if (childLeft >= parentLeft && childRight <= parentRight
   1309                 && childTop >= parentTop && childBottom <= parentBottom) {
   1310             return true;
   1311         }
   1312         return false;
   1313     }
   1314 }
   1315