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